TomlTypeConverter.cs 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Globalization;
  4. using System.Runtime.CompilerServices;
  5. using System.Text;
  6. using System.Text.RegularExpressions;
  7. using UnityEngine;
  8. using Logger = BepInEx.Logging.Logger;
  9. namespace BepInEx.Configuration
  10. {
  11. /// <summary>
  12. /// Serializer/deserializer used by the config system.
  13. /// </summary>
  14. public static class TomlTypeConverter
  15. {
  16. // Don't put anything from UnityEngine here or it will break preloader, use LazyTomlConverterLoader instead
  17. private static Dictionary<Type, TypeConverter> TypeConverters { get; } = new Dictionary<Type, TypeConverter>
  18. {
  19. [typeof(string)] = new TypeConverter
  20. {
  21. ConvertToString = (obj, type) => Escape((string)obj),
  22. ConvertToObject = (str, type) =>
  23. {
  24. // Check if the string is a file path with unescaped \ path separators (e.g. D:\test and not D:\\test)
  25. if (Regex.IsMatch(str, @"^""?\w:\\(?!\\)(?!.+\\\\)"))
  26. return str;
  27. return Unescape(str);
  28. },
  29. },
  30. [typeof(bool)] = new TypeConverter
  31. {
  32. ConvertToString = (obj, type) => obj.ToString().ToLowerInvariant(),
  33. ConvertToObject = (str, type) => bool.Parse(str),
  34. },
  35. [typeof(byte)] = new TypeConverter
  36. {
  37. ConvertToString = (obj, type) => obj.ToString(),
  38. ConvertToObject = (str, type) => byte.Parse(str),
  39. },
  40. //integral types
  41. [typeof(sbyte)] = new TypeConverter
  42. {
  43. ConvertToString = (obj, type) => obj.ToString(),
  44. ConvertToObject = (str, type) => sbyte.Parse(str),
  45. },
  46. [typeof(byte)] = new TypeConverter
  47. {
  48. ConvertToString = (obj, type) => obj.ToString(),
  49. ConvertToObject = (str, type) => byte.Parse(str),
  50. },
  51. [typeof(short)] = new TypeConverter
  52. {
  53. ConvertToString = (obj, type) => obj.ToString(),
  54. ConvertToObject = (str, type) => short.Parse(str),
  55. },
  56. [typeof(ushort)] = new TypeConverter
  57. {
  58. ConvertToString = (obj, type) => obj.ToString(),
  59. ConvertToObject = (str, type) => ushort.Parse(str),
  60. },
  61. [typeof(int)] = new TypeConverter
  62. {
  63. ConvertToString = (obj, type) => obj.ToString(),
  64. ConvertToObject = (str, type) => int.Parse(str),
  65. },
  66. [typeof(uint)] = new TypeConverter
  67. {
  68. ConvertToString = (obj, type) => obj.ToString(),
  69. ConvertToObject = (str, type) => uint.Parse(str),
  70. },
  71. [typeof(long)] = new TypeConverter
  72. {
  73. ConvertToString = (obj, type) => obj.ToString(),
  74. ConvertToObject = (str, type) => long.Parse(str),
  75. },
  76. [typeof(ulong)] = new TypeConverter
  77. {
  78. ConvertToString = (obj, type) => obj.ToString(),
  79. ConvertToObject = (str, type) => ulong.Parse(str),
  80. },
  81. //floating point types
  82. [typeof(float)] = new TypeConverter
  83. {
  84. ConvertToString = (obj, type) => ((float)obj).ToString(NumberFormatInfo.InvariantInfo),
  85. ConvertToObject = (str, type) => float.Parse(str, NumberFormatInfo.InvariantInfo),
  86. },
  87. [typeof(double)] = new TypeConverter
  88. {
  89. ConvertToString = (obj, type) => ((double)obj).ToString(NumberFormatInfo.InvariantInfo),
  90. ConvertToObject = (str, type) => double.Parse(str, NumberFormatInfo.InvariantInfo),
  91. },
  92. [typeof(decimal)] = new TypeConverter
  93. {
  94. ConvertToString = (obj, type) => ((decimal)obj).ToString(NumberFormatInfo.InvariantInfo),
  95. ConvertToObject = (str, type) => decimal.Parse(str, NumberFormatInfo.InvariantInfo),
  96. },
  97. //enums are special
  98. [typeof(Enum)] = new TypeConverter
  99. {
  100. ConvertToString = (obj, type) => obj.ToString(),
  101. ConvertToObject = (str, type) => Enum.Parse(type, str, true),
  102. },
  103. };
  104. /// <summary>
  105. /// Convert object of a given type to a string using available converters.
  106. /// </summary>
  107. public static string ConvertToString(object value, Type valueType)
  108. {
  109. var conv = GetConverter(valueType);
  110. if (conv == null)
  111. throw new InvalidOperationException($"Cannot convert from type {valueType}");
  112. return conv.ConvertToString(value, valueType);
  113. }
  114. /// <summary>
  115. /// Convert string to an object of a given type using available converters.
  116. /// </summary>
  117. public static T ConvertToValue<T>(string value)
  118. {
  119. return (T)ConvertToValue(value, typeof(T));
  120. }
  121. /// <summary>
  122. /// Convert string to an object of a given type using available converters.
  123. /// </summary>
  124. public static object ConvertToValue(string value, Type valueType)
  125. {
  126. var conv = GetConverter(valueType);
  127. if (conv == null)
  128. throw new InvalidOperationException($"Cannot convert to type {valueType.Name}");
  129. return conv.ConvertToObject(value, valueType);
  130. }
  131. /// <summary>
  132. /// Get a converter for a given type if there is any.
  133. /// </summary>
  134. public static TypeConverter GetConverter(Type valueType)
  135. {
  136. if (valueType == null) throw new ArgumentNullException(nameof(valueType));
  137. if (valueType.IsEnum)
  138. return TypeConverters[typeof(Enum)];
  139. if (!TypeConverters.TryGetValue(valueType, out var result) && !_lazyLoadedConverters)
  140. {
  141. _lazyLoadedConverters = true;
  142. LazyLoadConverters();
  143. TypeConverters.TryGetValue(valueType, out result);
  144. }
  145. return result;
  146. }
  147. /// <summary>
  148. /// Add a new type converter for a given type.
  149. /// If a different converter is already added, this call is ignored and false is returned.
  150. /// </summary>
  151. public static bool AddConverter(Type type, TypeConverter converter)
  152. {
  153. if (type == null) throw new ArgumentNullException(nameof(type));
  154. if (converter == null) throw new ArgumentNullException(nameof(converter));
  155. if (CanConvert(type))
  156. {
  157. Logger.LogWarning("Tried to add a TomlConverter when one already exists for type " + type.FullName);
  158. return false;
  159. }
  160. TypeConverters.Add(type, converter);
  161. return true;
  162. }
  163. /// <summary>
  164. /// Check if a given type can be converted to and from string.
  165. /// </summary>
  166. public static bool CanConvert(Type type)
  167. {
  168. return GetConverter(type) != null;
  169. }
  170. /// <summary>
  171. /// Give a list of types with registered converters.
  172. /// </summary>
  173. public static IEnumerable<Type> GetSupportedTypes()
  174. {
  175. return TypeConverters.Keys;
  176. }
  177. private static bool _lazyLoadedConverters;
  178. private static void LazyLoadConverters()
  179. {
  180. try { LazyTomlConverterLoader.AddUnityEngineConverters(); }
  181. catch (Exception ex) { Logger.LogWarning("Failed to load UnityEngine Toml converters - " + ex.Message); }
  182. }
  183. private static string Escape(this string txt)
  184. {
  185. if (string.IsNullOrEmpty(txt)) return string.Empty;
  186. var stringBuilder = new StringBuilder(txt.Length + 2);
  187. foreach (char c in txt)
  188. switch (c)
  189. {
  190. case '\0':
  191. stringBuilder.Append(@"\0");
  192. break;
  193. case '\a':
  194. stringBuilder.Append(@"\a");
  195. break;
  196. case '\b':
  197. stringBuilder.Append(@"\b");
  198. break;
  199. case '\t':
  200. stringBuilder.Append(@"\t");
  201. break;
  202. case '\n':
  203. stringBuilder.Append(@"\n");
  204. break;
  205. case '\v':
  206. stringBuilder.Append(@"\v");
  207. break;
  208. case '\f':
  209. stringBuilder.Append(@"\f");
  210. break;
  211. case '\r':
  212. stringBuilder.Append(@"\r");
  213. break;
  214. case '\'':
  215. stringBuilder.Append(@"\'");
  216. break;
  217. case '\\':
  218. stringBuilder.Append(@"\");
  219. break;
  220. case '\"':
  221. stringBuilder.Append(@"\""");
  222. break;
  223. default:
  224. stringBuilder.Append(c);
  225. break;
  226. }
  227. return stringBuilder.ToString();
  228. }
  229. private static string Unescape(this string txt)
  230. {
  231. if (string.IsNullOrEmpty(txt))
  232. return txt;
  233. var stringBuilder = new StringBuilder(txt.Length);
  234. for (int i = 0; i < txt.Length;)
  235. {
  236. int num = txt.IndexOf('\\', i);
  237. if (num < 0 || num == txt.Length - 1)
  238. num = txt.Length;
  239. stringBuilder.Append(txt, i, num - i);
  240. if (num >= txt.Length)
  241. break;
  242. char c = txt[num + 1];
  243. switch (c)
  244. {
  245. case '0':
  246. stringBuilder.Append('\0');
  247. break;
  248. case 'a':
  249. stringBuilder.Append('\a');
  250. break;
  251. case 'b':
  252. stringBuilder.Append('\b');
  253. break;
  254. case 't':
  255. stringBuilder.Append('\t');
  256. break;
  257. case 'n':
  258. stringBuilder.Append('\n');
  259. break;
  260. case 'v':
  261. stringBuilder.Append('\v');
  262. break;
  263. case 'f':
  264. stringBuilder.Append('\f');
  265. break;
  266. case 'r':
  267. stringBuilder.Append('\r');
  268. break;
  269. case '\'':
  270. stringBuilder.Append('\'');
  271. break;
  272. case '\"':
  273. stringBuilder.Append('\"');
  274. break;
  275. case '\\':
  276. stringBuilder.Append('\\');
  277. break;
  278. default:
  279. stringBuilder.Append('\\').Append(c);
  280. break;
  281. }
  282. i = num + 2;
  283. }
  284. return stringBuilder.ToString();
  285. }
  286. }
  287. /// <summary>
  288. /// For types that are in assemblies that can't get loaded before preloader runs (or it won't work on these assemblies)
  289. /// </summary>
  290. internal static class LazyTomlConverterLoader
  291. {
  292. [MethodImpl(MethodImplOptions.NoInlining)]
  293. public static void AddUnityEngineConverters()
  294. {
  295. var colorConverter = new TypeConverter
  296. {
  297. ConvertToString = (obj, type) => ColorUtility.ToHtmlStringRGBA((Color)obj),
  298. ConvertToObject = (str, type) =>
  299. {
  300. if (!ColorUtility.TryParseHtmlString("#" + str.Trim('#', ' '), out var c))
  301. throw new FormatException("Invalid color string, expected hex #RRGGBBAA");
  302. return c;
  303. },
  304. };
  305. TomlTypeConverter.AddConverter(typeof(Color), colorConverter);
  306. var jsonConverter = new TypeConverter
  307. {
  308. ConvertToString = (obj, type) => JsonUtility.ToJson(obj),
  309. ConvertToObject = (str, type) => JsonUtility.FromJson(type: type, json: str),
  310. };
  311. TomlTypeConverter.AddConverter(typeof(Vector2), jsonConverter);
  312. TomlTypeConverter.AddConverter(typeof(Vector3), jsonConverter);
  313. TomlTypeConverter.AddConverter(typeof(Vector4), jsonConverter);
  314. TomlTypeConverter.AddConverter(typeof(Quaternion), jsonConverter);
  315. }
  316. }
  317. }