chr
2026-04-05 fe750b791d5b517cc4e9bc8e99a9a75139a0cfba
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
//            Copyright Keysight Technologies 2012-2019
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at http://mozilla.org/MPL/2.0/.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using System.Reflection;
using System.Globalization;
using System.Xml.Serialization;
using System.ComponentModel;
using System.Collections;
using System.Xml;
 
namespace OpenTap.Plugins
{
 
    /// <summary>
    /// Implemented by serializer plugins that creates and populates members of an object.
    /// </summary>
    public interface IConstructingSerializer
    {
        /// <summary> The object currently being serialized/deserialized. </summary>
        object Object { get; }
        /// <summary> Optionally set to indicate which member of Object is being serialized/deserialized. </summary>
        IMemberData CurrentMember { get; }
    }
    /// <summary>
    /// Default object serializer.
    /// </summary>
    internal class ObjectSerializer : TapSerializerPlugin, ITapSerializerPlugin, IConstructingSerializer
    {
        /// <summary>
        /// Gets the member currently being serialized.
        /// </summary>
        public IMemberData CurrentMember { get; private set; }
        public static XName IgnoreMemberXName = "ignore-member";
 
        /// <summary>
        /// Specifies order. Minimum order should  be -1 as this is the most basic serializer.  
        /// </summary>
        public override double Order
        {
            get { return -1; }
        }
        
        /// <summary> The currently serializing or deserializing object. </summary>
        public object Object { get; private set; }
 
        Dictionary<ITypeData, IMemberData[]> serializableMembers = new Dictionary<ITypeData, IMemberData[]>();
        
        /// <summary>
        /// Tries to deserialize an object from an XElement.
        /// </summary>
        /// <param name="element"></param>
        /// <param name="t"></param>
        /// <param name="setter"></param>
        /// <param name="newobj"></param>
        /// <param name="logWarnings">Whether warning messages should be emitted in case of missing properties.</param>
        /// <returns>True on success.</returns>
        public virtual bool TryDeserializeObject(XElement element, ITypeData t, Action<object> setter, object newobj = null, bool logWarnings = true)
        {
            
            if (element.IsEmpty && !element.HasAttributes)
            {
                setter(null);
                return true;
            }
            if (newobj == null)
            {
                if (t.CanCreateInstance == false)
                {
                    // if the instance type cannot be constructed,
                    // use the instance already on the object.
                    var objectSerializer = Serializer.SerializerStack.OfType<ObjectSerializer>().FirstOrDefault();
                    var ownerMember = objectSerializer?.CurrentMember;
                    var ownerObj = objectSerializer?.Object;
                    if (ownerMember == null || ownerObj == null)
                    {
                        throw new Exception($"Cannot create instance of {t} and no default value exists.");
                    }
                    if (ownerMember.HasAttribute<IFactoryAttribute>())
                    {
                        // Use element factory if the member type is not the target type, but rather 'has a' target type.
                        // we dont need to detect if it 'has a' target type, lets just assume it.
                        if (ownerMember.TypeDescriptor.DescendsTo(t) && ownerMember.GetAttribute<FactoryAttribute>() is FactoryAttribute f)
                        {
                            newobj = FactoryAttribute.Create(ownerObj, f);
                        }
                        else if (ownerMember.GetAttribute<ElementFactoryAttribute>() is ElementFactoryAttribute f2)
                        {
                            newobj = FactoryAttribute.Create(ownerObj, f2);
                        }
                    }
 
                    if (newobj == null)
                    {
                        newobj = ownerMember.GetValue(ownerObj);
                        if (newobj == null)
                            throw new Exception($"Unable to get default value of {ownerMember}");
                    }
                }
                else
                {
                    newobj = t.CreateInstance(Array.Empty<object>());
                }
            }
            
            var prevobj = Object;
            Object = newobj;
            
            if (newobj == null)
                throw new ArgumentNullException(nameof(newobj));
            t = TypeData.GetTypeData(newobj);
            var t2 = t;
            var properties = serializableMembers.GetOrCreateValue(t2, t3 => t3.GetMembers()
                .Where(x => x.HasAttribute<XmlIgnoreAttribute>() == false)
                .ToArray());
 
            try
            {
                
                foreach (var prop in properties)
                {
                    var attr = prop.GetAttribute<XmlAttributeAttribute>();
                    if (attr == null) continue;
                    var name = string.IsNullOrWhiteSpace(attr.AttributeName) ? prop.Name : attr.AttributeName;
                    var attr_value = element.Attribute(Serializer.PropertyXmlName(name));
                    
 
                    if (attr_value != null)
                    {
                        try
                        {
                            var typeData = prop.TypeDescriptor.AsTypeData();
                            readContentInternal(typeData.Type, false, () => attr_value.Value, element, out object value);
                            
                            prop.SetValue(newobj, value);
                            
                        }
                        catch (Exception e)
                        {
                            if (logWarnings)
                            {
                                Log.Warning(element, "Attribute value '{0}' was not read correctly as a {1}", attr_value.Value, prop);
                                Log.Debug(e);
                            }
                        }
                    }
                }
 
                if (properties.FirstOrDefault(x => x.HasAttribute<XmlTextAttribute>()) is IMemberData mem2)
                {
                    object value;
                    if (mem2.TypeDescriptor is TypeData td &&
                        readContentInternal(td.Load(), false, () => element.Value, element, out object _value))
                    { value = _value; } 
                    else
                        value = StringConvertProvider.FromString(element.Value, mem2.TypeDescriptor, null);
                    mem2.SetValue(newobj, value);
                }
                else
                {
                    var props = properties.ToLookup(x => x.GetAttributes<XmlElementAttribute>().FirstOrDefault()?.ElementName ?? x.Name);
                    var elements = element.Elements().ToArray();
                    bool[] visited = new bool[elements.Length];
                    
                    double order = 0;
                    int foundWithCurrentType = 0;
                    while (true)
                    {
                        double nextOrder = 1000;
                        // since the object might be dynamically adding properties as other props are added.
                        // we need to iterate a bit. Example: Test Plan Reference.
 
                        int found = visited.Count(x => x);
                        for (int i = 0; i < elements.Length; i++)
                        {
                            var element2 = elements[i];
                            if (visited[i]) continue;
                            if (element2.Attribute(IgnoreMemberXName) is XAttribute attr && attr.Value == "true")
                            {
                                visited[i] = true;
                                continue;
                            }
                            IMemberData property = null;
                            var name = XmlConvert.DecodeName(element2.Name.LocalName);
                            var propertyMatches = props[name];
 
                            int hits = 0;
                            foreach (var p in propertyMatches)
                            {
                                if (p.Writable || p.HasAttribute<XmlIgnoreAttribute>())
                                {
                                    property = p;
                                    hits++;
                                }
                            }
 
                            if (property == null)
                            {
                                continue; // later we might try this property again.
                            }
                            if (hits > 1)
                                Log.Warning(element2, "Multiple properties named '{0}' are available to the serializer in '{1}' this might give issues in serialization.", element2.Name.LocalName, t.GetAttribute<DisplayAttribute>().Name);
 
                            if (property.GetAttribute<DeserializeOrderAttribute>() is DeserializeOrderAttribute orderAttr)
                            {
                                if (order < orderAttr.Order)
                                {
                                    if (orderAttr.Order < nextOrder)
                                    {
                                        nextOrder = orderAttr.Order;
                                    }
                                    continue;
                                }
                            }
                            else
                            {
                                nextOrder = order;
                            }
 
                            visited[i] = true;
                            var prev = CurrentMember;
                            CurrentMember = property;
                            try
                            {
                                if (CurrentMember.HasAttribute<XmlIgnoreAttribute>()) // This property shouldn't have been in the file in the first place, but in case it is (because of a change or a bug), we shouldn't try to set it. (E.g. SweepLoopRange.SweepStep)
                                    if (!CurrentMember.HasAttribute<BrowsableAttribute>()) // In the special case we assume that this is some compatibility property that still needs to be set if present in the XML. (E.g. TestPlanReference.DynamicDataContents)
                                        continue;
 
                                if (property is MemberData mem && mem.Member is PropertyInfo Property && Property.PropertyType.HasInterface<IList>() && Property.PropertyType.IsGenericType && Property.HasAttribute<XmlElementAttribute>())
                                {
                                    // Special case to mimic old .NET XmlSerializer behavior
                                    var list = (IList)Property.GetValue(newobj);
                                    Action<object> setValue = x => list.Add(x);
                                    Serializer.Deserialize(element2, setValue, Property.PropertyType.GetGenericArguments().First());
                                }
                                else
                                {
                                    void setValue(object x)
                                    {
                                        property.SetValue(newobj, x);
                                    }
                                    
                                    if (property.HasAttribute<DeserializeInPlaceAttribute>())
                                    {
                                        var current = property.GetValue(newobj);
                                        if (current == null)
                                            throw new Exception($"Unable to deserialize {property} in-place.");
                                        this.TryDeserializeObject(element2, TypeData.GetTypeData(current), (x) => { },
                                            current, true);
                                    }
                                    else
                                    {
                                        Serializer.Deserialize(element2, setValue, property.TypeDescriptor);    
                                    }
                                }
                            }
                            catch (Exception e)
                            {
                                if (logWarnings)
                                {
                                    Log.Warning(element2, "Unable to set property '{0}'.", CurrentMember.Name);
                                    Log.Warning("Error was: \"{0}\".", e.Message);
                                    Log.Debug(e);
                                }
                            }
                            finally
                            {
                                CurrentMember = prev;
                            }
                        }
 
                        int nowFound = visited.Count(x => x);
                        if (found == nowFound && order == nextOrder)
                        {
                            // The might have changed by loading properties.
                            var t3 = TypeData.GetTypeData(newobj);
                            if (Equals(t3, t2) == false)
                            {
                                if (nowFound != foundWithCurrentType) 
                                {  //  check avoids infinite loop if ITypeData did not overload Equals.
                                    
                                    foundWithCurrentType = nowFound;
                                    t2 = t3;
                                    continue;
                                }
                            }
                            if (nowFound < elements.Length)
                            { // still elements left to check. Last resort to try loading at defer.
 
                                // Some of the items might not be deserializable before defer load.
                                // if this is the case do this as a defer action.
                                void postDeserialize()
                                {
                                    t2 = TypeData.GetTypeData(newobj);
                                    for(int j = 0; j < elements.Length; j++)
                                    {
                                        if (visited[j]) continue;
                                        var propertyElement = elements[j];
                                        IMemberData property = null;
                                        var elementName = XmlConvert.DecodeName(propertyElement.Name.LocalName);
                                        try
                                        {   
                                            property = t2.GetMember(elementName);
                                            if (property == null)
                                                property = t2.GetMembers().FirstOrDefault(x => x.Name == elementName);
                                        }
                                        catch { }
                                        if (property == null || property.Writable == false)
                                        {
                                            continue;
                                        }
                                        
                                        Action<object> setValue = x =>
                                        {
                                            // Defer: this fixes an issue when `property` is ParameterMemberData,
                                            // and the object being set has properties which are also deferred.
                                            // This must be deferred because ParameterMemberData relies on IObjectCloner
                                            // to clone the member value, but if this setter is not deferred, the objects
                                            // will have been cloned before they are fully serialized
                                            var priority = property is ParameterMemberData
                                                ? TapSerializer.DeferredLoadOrder.ParameterMemberDataSetter
                                                : TapSerializer.DeferredLoadOrder.Normal;
                                            Serializer.DeferLoad(() => property.SetValue(newobj, x), priority);
                                        };
                                        
                                        var prevobj2 = Object;
                                        var prevmember = this.CurrentMember;
                                        Object = newobj;
                                        CurrentMember = property;
                                        // at this point the serializer stack is technically empty,
                                        // so add the current on top and deserialize.
                                        Serializer.PushActiveSerializer(this);
                                        try
                                        {
                                            Serializer.Deserialize(propertyElement, setValue, property.TypeDescriptor);
                                        }
                                        finally
                                        {
                                            // clean up.
                                            Serializer.PopActiveSerializer();
                                            Object = prevobj2;
                                            CurrentMember = prevmember;
                                        }
 
                                        visited[j] = true;
                                    }
                                    // print a warning message if the element could not be deserialized.
                                    for (int j = 0; j < elements.Length; j++)
                                    {
                                        if (visited[j]) continue;
                                        var elem = elements[j];
                                        var elementName = elem.Name.LocalName;
                                        if (elementName.Contains('.') == false)
                                        {
                                            // if the element name contains '.' it is usually a special name and hence
                                            // an error message is not needed. e.g:
                                            //     Package.Dependencies
                                            //     TestStep.Inputs
                                            var message =
                                                $"Unable to read element '{elem.Name.LocalName}'. The property does not exist.";
                                            Serializer.PushError(elem, message);
                                        }
                                    }
                                }
                                Serializer.DeferLoad(postDeserialize);
                            }
                            break;
                        }
                        order = nextOrder;
                    }
                }
                setter(newobj);
            }
            finally
            {
                Object = prevobj;
                
                if (newobj is IDeserializedCallback)
                {
                    // Callback should be added in the end because an inner part of the object might also have defered it.
                    Serializer.DeferLoad(() =>
                    {
                        try
                        {
                            ((IDeserializedCallback)newobj).OnDeserialized();
                        }
                        catch (Exception e)
                        {
                            Log.Warning("Exception caught while handling OnSerialized");
                            Log.Debug(e);
                        }
                    });
                }
            }
            return true;
        }
 
        bool readContentInternal(Type propType, bool ignoreComponentSettings, Func<string> getvalueString, XElement elem, out object outvalue)
        {
            
            object value = null;
            bool ok;
 
            if (propType.IsEnum || propType.IsPrimitive || propType == typeof(string) || propType.IsValueType || propType == typeof(Type))
            {  
                ok = true;
                string valueString = getvalueString()?.Trim();
 
                if (propType.IsEnum)
                {
                    if (!string.IsNullOrEmpty(valueString))
                    {
                        // legacy support: A flagged enum did not have ','s, but just spaces between.
                        if (!valueString.Contains(','))
                        {
                            var splitted = valueString.Split(' ');
                            value = Enum.Parse(propType, string.Join(",", splitted));
                        }
                        else
                            value = Enum.Parse(propType, valueString);
                    }
                }
 
                else if (propType == typeof(String) || propType == typeof(char))
                {
                    if (elem.HasElements)
                    {
                        // string contains Base64 if it has invalid XML chars.
                        // elem.Element("Base64") fails if the attribute "xmlns" is set on the document.
                        // In this case, "Base64" must be prepended with the value of xmlns in brackets.
                        // If the namespace is not set, ns.GetName("Base64") will just evaluate to "Base64"
                        var ns = elem.GetDefaultNamespace();
                        var encode = elem.Element(ns.GetName("Base64"));
                        if (encode != null)
                        {
                            try
                            {
                                value = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(encode.Value));
                            }
                            catch
                            {
                                value = encode.Value;
                            }
                        }
                    }
                    if (value == null)
                        value = valueString;
                    if (propType == typeof(char))
                        value = ((string) value)[0];
                }
                else if (propType == typeof(TimeSpan) || propType == typeof(TimeSpan?))
                {
                    value = TimeSpan.Parse(valueString, CultureInfo.InvariantCulture);
                }
                else if (propType == typeof(Guid) || propType == typeof(Guid?))
                {
                    value = Guid.Parse(valueString);
                }
                else if (propType == typeof(Type))
                {
                    value = PluginManager.LocateType(valueString.Split(',').First());
                }
                else if (tryConvertNumber(propType, valueString, out value))
                {
                    // Conversions done in tryConvertNumber
                }
                else if (propType.HasInterface<IConvertible>())
                {
                    value = Convert.ChangeType(valueString, propType, CultureInfo.InvariantCulture);
                }else if (propType.IsGenericType && propType.GetGenericTypeDefinition() == typeof(Nullable<>))
                {
                    // For nullable<T> we recurse focusing on the underlying non-nullable type.
                    if (valueString == null)
                    {
                        outvalue = null;
                        return true; // null is a valid value for nullable types.
                    }
 
                    return readContentInternal(Nullable.GetUnderlyingType(propType), ignoreComponentSettings, getvalueString, elem, out outvalue);
                }
                else
                {
                    ok = false;
                }
            }
            else
            {
                ok = false;
            }
 
            outvalue = value;
            return ok;
        }
        
        static bool tryConvertNumber(Type type, string valueString, out object value)
        {
            switch (Type.GetTypeCode(type))
            {
                case TypeCode.Byte:
                {
                    byte oval;
                    bool ok = byte.TryParse(valueString, NumberStyles.Any, CultureInfo.InvariantCulture, out oval);
                    value = oval;
                    return ok;
                }
                case TypeCode.SByte:
                {
                    SByte oval;
                    bool ok = SByte.TryParse(valueString, NumberStyles.Any, CultureInfo.InvariantCulture, out oval);
                    value = oval;
                    return ok;
                }
                case TypeCode.UInt16:
                {
                    UInt16 oval;
                    bool ok = UInt16.TryParse(valueString, NumberStyles.Any, CultureInfo.InvariantCulture, out oval);
                    value = oval;
                    return ok;
                }
                case TypeCode.UInt32:
                {
                    UInt32 oval;
                    bool ok = UInt32.TryParse(valueString, NumberStyles.Any, CultureInfo.InvariantCulture, out oval);
                    value = oval;
                    return ok;
                }
                case TypeCode.UInt64:
                {
                    UInt64 oval;
                    bool ok = UInt64.TryParse(valueString, NumberStyles.Any, CultureInfo.InvariantCulture, out oval);
                    value = oval;
                    return ok;
                }
                    
                case TypeCode.Int16:
                {
                    Int16 oval;
                    bool ok = Int16.TryParse(valueString, NumberStyles.Any, CultureInfo.InvariantCulture, out oval);
                    value = oval;
                    return ok;
                }
                case TypeCode.Int32:
                {
                    Int32 oval;
                    bool ok = Int32.TryParse(valueString, NumberStyles.Any, CultureInfo.InvariantCulture, out oval);
                    value = oval;
                    return ok;
                }
                case TypeCode.Int64:
                {
                    Int64 oval;
                    bool ok = Int64.TryParse(valueString, NumberStyles.Any, CultureInfo.InvariantCulture, out oval);
                    value = oval;
                    return ok;
                }
                case TypeCode.Decimal:
                {
                    value = BigFloat.Convert(valueString, CultureInfo.InvariantCulture).ConvertTo(typeof(decimal));
                    return true;
                }
                case TypeCode.Double:
                {
                    Double oval;
                    bool ok = Double.TryParse(valueString, NumberStyles.Any, CultureInfo.InvariantCulture, out oval);
                    value = oval;
                    return ok;
                }
                case TypeCode.Single:
                {
                    Single oval;
                    bool ok = Single.TryParse(valueString, NumberStyles.Any, CultureInfo.InvariantCulture, out oval);
                    value = oval;
                    return ok;
                }
                default:
                    value = null;
                    return false;
            }
        }
 
        // for detecting cycles in object serialization.
        HashSet<object> cycleDetetionSet = new HashSet<object>();
 
        bool AlwaysG17DoubleFormat = false;
 
        internal static readonly XName DefaultValue = "DefaultValue";
 
        /// <summary>
        /// Deserializes an object from XML.
        /// </summary>
        /// <param name="element"></param>
        /// <param name="t"></param>
        /// <param name="setter"></param>
        /// <returns></returns>
        public override bool Deserialize(XElement element, ITypeData t, Action<object> setter)
        {
            try
            {
                if (t is TypeData ctd)
                {
                    if (readContentInternal(ctd.Type, false, () => element.IsEmpty ? null : element.Value, element, out object obj))
                    {
                        setter(obj);
                        return true;
                    }
                }
                if (TryDeserializeObject(element, t, setter))
                    return true;
            }
            catch (Exception ex) 
            {
                Serializer.HandleError(element, $"Unable to read {t.GetDisplayAttribute().GetFullName()}.", ex);
            }
            return false;
        }
        /// <summary>
        /// Checks if the string can be turned into an XML string. 
        /// This should return true only if the transformation to/from xml is reversible.
        /// </summary>
        /// <param name="str"></param>
        /// <returns></returns>
        static bool containsOnlyReversibleTapXmlChars(string str)
        {
            int len = str.Length;
            for(int i = 0; i < len; i++)
            {
                char c = str[i];
                if (c == '\r') // special case. Somehow deserialization turns \n into \r.
                    return false;
                if (char.IsLetter(c))
                    continue;
                if (XmlConvert.IsXmlChar(c))
                    continue;
                if(i < len - 1)
                {
                    char c2 = str[i + 1];
                    if(XmlConvert.IsXmlSurrogatePair(c2, c))
                        continue;
                }
                return false;
                
            }
            return true;
        }
 
        private List<string> GetUsedFiles(object obj)
        {
            var result = new List<string>();
 
            string GetStringOrMacroString(IMemberData prop, object o)
            {
                if (prop.HasAttribute<XmlIgnoreAttribute>())
                    return null;
                object val = prop.GetValue(o);
 
                if (val is string s)
                    return s;
                if (val is MacroString m)
                    return m.ToString();
 
                return null;
            }
 
            var td = TypeData.GetTypeData(obj);
 
            var props = td.GetMembers().Where(m => m.HasAttribute<FileDependencyAttribute>());
            foreach (var prop in props)
            {
                var path = GetStringOrMacroString(prop, obj);
 
                if (string.IsNullOrWhiteSpace(path) == false)
                    result.Add(path);
            }
 
            return result;
        }
 
        // this 'cache' only caches serialized values during this instance of the serialize
        // this is to avoid a memory leak for e.g GUIDs, which falls into the category of primitives.
        readonly Dictionary<object, string> enumTable = new Dictionary<object, string>();
 
        /// <summary>
        /// Serializes an object to XML.
        /// </summary>
        /// <param name="elem"></param>
        /// <param name="obj"></param>
        /// <param name="expectedType"></param>
        /// <returns></returns>
        public override bool Serialize(XElement elem, object obj, ITypeData expectedType)
        {
            if (obj == null)
                return true;
 
            object prevObj = Object;
            // If cycleDetectorLut already contains an element, we've been here before.
            // Note the cycleDetectorLut adds and removes the obj later if it is not already in it.
            if (!cycleDetetionSet.Add(obj))
                throw new Exception("Cycle detected");
            
            foreach (var fileResource in GetUsedFiles(obj))
                Serializer.NotifyFileUsed(fileResource);
            
            object obj2 = obj;
            try
            {
                Object = obj;
                switch (obj)
                {
                    case double d:
                        if (AlwaysG17DoubleFormat)
                        {
                            // the fastest and most accurate string representation of a double.
                            elem.Value = d.ToString("G17", CultureInfo.InvariantCulture);
                            return true;
                        }
                        else
                        {
                            // It was decided to use R instead of G17 for readability, although G17 is slightly faster.
                            // however, there is a bug in "R" formatting on some .NET versions, that means that 
                            // roundtrip actually does not work.
                            // See section "Note to Callers:" at https://msdn.microsoft.com/en-us/library/kfsatb94(v=vs.110).aspx
                            // so here we format and then parse back to see if it can actually roundtrip.
                            // if not, we format with G17.
                            var d_str = d.ToString("R", CultureInfo.InvariantCulture);
                            var d_re = double.Parse(d_str, CultureInfo.InvariantCulture);
                            if (d_re != d) 
                                // round trip not possible with R, use G17 instead.
                                d_str = d.ToString("G17", CultureInfo.InvariantCulture);
                            elem.Value = d_str;
 
                            return true;
                        }
                    case float f:
                        elem.Value = f.ToString("R", CultureInfo.InvariantCulture);
                        return true;
                    case decimal d:
                        elem.Value = BigFloat.Convert(d).ToString(CultureInfo.InvariantCulture);
                        return true;
                    case char c:
                        // Convert to a string and then handle it later.
                        obj = Convert.ToString(c, CultureInfo.InvariantCulture);
                        break;
                }
 
                if (obj is string str) // handled separately due to "case char c" from above.
                {
                    // check if the str->xml is reversible otherwise base64 encode it.
                    if (containsOnlyReversibleTapXmlChars(str) && str.Trim() == str)
                    {
                        elem.Value = str;   
                    }
                    else
                    {
                        var subelem = new XElement("Base64", Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(str)));
                        elem.Add(subelem);
                    }
                    return true;
                }
 
                if (expectedType is TypeData type)
                {
                    if(type.Type.IsEnum || type.Type.IsPrimitive || type.Type.IsValueType)
                    {
                        if (type.Type == typeof(bool))
                        {
                            elem.Value =
                                (bool)obj ? "true" : "false"; // must be lower case for old XmlSerializer to work
                            return true;
                        }
 
                        if (enumTable.TryGetValue(obj, out var v))
                        {
                            elem.Value = v;
                            return true;
                        }
                        v = Convert.ToString(obj, CultureInfo.InvariantCulture);
                        enumTable[obj] = v;
                        elem.Value = v;
                        return true;
                    }
                }
 
                IMemberData xmlTextProp = null;
                var _type = TypeData.GetTypeData(obj);
                var properties = _type.GetMembers();
                foreach (IMemberData prop in properties)
                {
                    if (prop.HasAttribute<XmlIgnoreAttribute>()) continue;
                    if (prop.HasAttribute<XmlTextAttribute>())
                        xmlTextProp = prop;
                    var attr = prop.GetAttribute<XmlAttributeAttribute>();
                    if (attr != null)
                    {
                        var val = prop.GetValue(obj);
                        var defaultAttr = prop.GetAttribute<DefaultValueAttribute>();
                        var name = string.IsNullOrWhiteSpace(attr.AttributeName) ? prop.Name : attr.AttributeName;
                        if (defaultAttr != null && object.Equals(defaultAttr.Value, val))
                            continue;
                        string valStr = Convert.ToString(val, CultureInfo.InvariantCulture);
                        if (val is bool b)
                            valStr = b ? "true" : "false"; // must be lower case for old XmlSerializer to work
                        elem.SetAttributeValue(Serializer.PropertyXmlName(name), valStr);
                    }
                }
 
                if (xmlTextProp != null)
                { // XmlTextAttribute support
                    var text = xmlTextProp.GetValue(obj);
                    if (text != null)
                        Serializer.Serialize(elem, text, xmlTextProp.TypeDescriptor);
                }
                else
                {
                    foreach (IMemberData subProp in properties)
                    {
                        if (subProp.HasAttribute<XmlIgnoreAttribute>()) continue;
                        if (subProp.Readable && subProp.Writable && null == subProp.GetAttribute<XmlAttributeAttribute>())
                        {
                            var oldProp = CurrentMember;
                            CurrentMember = subProp;
                            try
                            {
                                object val = subProp.GetValue(obj);
                                
                                var enu = val as IEnumerable;
                                if (enu != null && enu.GetEnumerator().MoveNext() == false) // the value is an empty IEnumerable
                                {
                                    var defaultAttr = subProp.GetAttribute<DefaultValueAttribute>();
                                    if (defaultAttr != null && defaultAttr.Value == null)
                                        continue;
                                }
 
                                var attr = subProp.GetAttribute<XmlElementAttribute>();
                                if (subProp.TypeDescriptor is TypeData cst && cst.Type.HasInterface<IList>() &&
                                    cst.Type.IsGenericType && attr != null)
                                {
                                    // Special case to mimic old .NET XmlSerializer behavior
                                    foreach (var item in enu)
                                    {
                                        string name = attr.ElementName ?? subProp.Name;
                                        XElement elem2 = new XElement(Serializer.PropertyXmlName(name));
                                        SetHasDefaultValueAttribute(subProp, item, elem2);
                                        elem.Add(elem2);
                                        Serializer.Serialize(elem2, item, TypeData.FromType(cst.Type.GetGenericArguments().First()));
                                    }
                                }
                                else
                                {
                                    XElement elem2 = new XElement(Serializer.PropertyXmlName(subProp.Name));
                                    
                                    { // MetaDataAttribute -> save the metadata name in the test plan xml.
                                        if (subProp.GetAttribute<MetaDataAttribute>() is MetaDataAttribute metaDataAttr)
                                        {
                                            string name = metaDataAttr.Name ??
                                                          subProp.GetDisplayAttribute()?.Name ?? subProp.Name;
                                            elem2.SetAttributeValue("Metadata", name);
                                        }
                                    }
                                    
                                    // if the setting has the default value, then don't notify that the type has been used, 
                                    // because it will probably not have been used in the end.
                                    bool hasDefaultValue =  SetHasDefaultValueAttribute(subProp, val, elem2);
                                    
                                    elem.Add(elem2);
                                    
                                    Serializer.Serialize(elem2, val, subProp.TypeDescriptor, notifyTypeUsed: !hasDefaultValue);
                                }
                            }
                            catch (Exception e)
                            {
                                Serializer.PushError(null, $"Unable to serialize property '{subProp.Name}'.", e);
                            }
                            finally
                            {
                                CurrentMember = oldProp;
                            }
                        }
                    }
                }
 
                if (elem.IsEmpty && obj != null)
                    elem.Value = "";
                return true;
            }
            finally
            {
                Object = prevObj;
                if (!cycleDetetionSet.Remove(obj2))
                    throw new InvalidOperationException("obj was modified.");
            }
        }
 
        bool SetHasDefaultValueAttribute(IMemberData subProp, object val, XElement elem2)
        {
            
            var attr = subProp.GetAttribute<DefaultValueAttribute>();
            if (attr != null && !(subProp is IParameterMemberData))
            {
                Serializer.GetSerializer<DefaultValueSerializer>().RegisterDefaultValue(elem2, attr.Value);
                return val == attr.Value;
            }
            return false;
        }
    }
}