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 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>(); 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 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())) { object val = prop.GetValue(obj); string name = prop.Name; var defaultValueAttr = prop.GetAttribute(); 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; } }