chr
2026-04-08 53e656200368a983e563550e2cc1acbc6d86b729
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
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Xml.Linq;
using System.Xml.Serialization;
 
namespace OpenTap.Package;
 
internal class PluginFileSerializerPlugin : TapSerializerPlugin
{
    private bool Deserialize2(XElement node, ITypeData t, Action<object> setter)
    {
        if (!(node.Name.LocalName == "Plugin" && t.IsA(typeof(PluginFile))))
            return false;
        var plugin = new PluginFile();
        foreach (var attr in node.Attributes())
        {
            switch (attr.Name.LocalName)
            {
                case "Type": plugin.Type = attr.Value; break;
                case "BaseType": plugin.BaseType = attr.Value; break;
            }
        }
 
        var manufacturersModels = new Dictionary<string, List<string>>();
        foreach (var elm in node.Elements())
        {
            switch (elm.Name.LocalName)
            {
                case "Name": plugin.Name = elm.Value; break;
                case "Order": plugin.Order = double.Parse(elm.Value, CultureInfo.InvariantCulture); break;
                case "Browsable": plugin.Browsable = bool.Parse(elm.Value); break;
                case "Description": plugin.Description = elm.Value; break;
                case "Collapsed": plugin.Collapsed = bool.Parse(elm.Value); break;
                case "Groups":
                    if (elm.HasElements)
                        Serializer.Deserialize(elm, o => plugin.Groups = o as string[], typeof(string[]));
                    else plugin.Groups = [];
                    break;
                case "Manufacturer":
                    var manufacturerName = elm.Attribute("Name").Value;
                    var models = elm.Elements().Select(x => x.Value).ToArray();
                    var lst = manufacturersModels.GetOrCreateValue(manufacturerName, _ => new());
                    lst.AddRange(models);
                    break;
            } 
        }
        plugin.SupportedModels =
            manufacturersModels.Select(kvp => new SupportedModelsAttribute(kvp.Key, kvp.Value.ToArray())).ToArray();
 
        setter.Invoke(plugin);
        return true; 
    }
    public override bool Deserialize(XElement node, ITypeData t, Action<object> setter)
    {
        // This is a workaround for a bug in older versions of OpenTAP which causes issues in upgrade scenarios.
        // When updating OpenTAP itself while running isolated, older versions of OpenTAP will detect that
        // the new version contains a new serializer plugin, which it will attempt to load. However,
        // this new serializer plugin depends on a newer version of `OpenTap.dll`, so using it will cause type load exceptions at runtime.
        try
        {
            return Deserialize2(node, t, setter);
        }
        catch (TypeLoadException)
        {
            // Fall back to the previous serializer logic. This should not cause issues since it will only happen
            // when this dll was loaded by an older version of OpenTAP
            return false;
        }
    }
    
    public static string SerializeDoubleWithRoundtrip(double d)
    {
        // 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);
        return d_str;
    }
 
    public override bool Serialize(XElement node, object obj, ITypeData expectedType)
    {
        if (expectedType.IsA(typeof(PluginFile)) == false) return false;
        foreach (IMemberData prop in expectedType.GetMembers().Where(s => !s.HasAttribute<XmlIgnoreAttribute>()))
        {
            object val = prop.GetValue(obj);
            string name = prop.Name;
            var defaultValueAttr = prop.GetAttribute<DefaultValueAttribute>();
            if (defaultValueAttr != null)
            {
                if (Object.Equals(defaultValueAttr.Value, val))
                    continue;
                if (defaultValueAttr.Value == null)
                {
                    if (val is IEnumerable enu && enu.IsEnumerableEmpty()) // the value is an empty IEnumerable
                    {
                        continue; // We take an empty IEnumerable to be the same as null
                    }
                }
            }
 
            switch (name)
            {
                case nameof(PluginFile.Type):
                case nameof(PluginFile.BaseType):
                    node.SetAttributeValue(name, val);
                    break;
                case nameof(PluginFile.Order):
                    node.Add(new XElement(name)
                    {
                        Value = SerializeDoubleWithRoundtrip((double)val),
                    });
                    break;
                case nameof(PluginFile.Name):
                case nameof(PluginFile.Browsable):
                case nameof(PluginFile.Description):
                case nameof(PluginFile.Collapsed):
                    node.Add(new XElement(name)
                    {
                        Value = val?.ToString() ?? ""
                    });
                    break;
                case nameof(PluginFile.Groups):
                    var lst = new XElement("Groups");
                    if (val is string[] strs)
                    {
                        foreach (var str in strs)
                        {
                            lst.Add(new XElement("String")
                            {
                                Value = str
                            });
                        }
                    }
                    node.Add(lst);
                    break;
                case nameof(PluginFile.SupportedModels):
                    if (val is SupportedModelsAttribute[] attrs)
                    {
                        var grp = attrs.GroupBy(x => x.Manufacturer);
                        foreach (var g in grp)
                        {
                            var man = new XElement("Manufacturer");
                            man.SetAttributeValue("Name", g.Key);
                            foreach (var model in g.SelectMany(x => x.Models))
                            {
                                var elm = new XElement("Model")
                                {
                                    Value = model
                                };
                                man.Add(elm);
                            }
 
                            node.Add(man);
                        }
                    }
 
                    break;
            }
        }
 
        return true;
    }
}