You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

398 lines
15 KiB

  1. #region License
  2. // Copyright (c) 2007 James Newton-King
  3. //
  4. // Permission is hereby granted, free of charge, to any person
  5. // obtaining a copy of this software and associated documentation
  6. // files (the "Software"), to deal in the Software without
  7. // restriction, including without limitation the rights to use,
  8. // copy, modify, merge, publish, distribute, sublicense, and/or sell
  9. // copies of the Software, and to permit persons to whom the
  10. // Software is furnished to do so, subject to the following
  11. // conditions:
  12. //
  13. // The above copyright notice and this permission notice shall be
  14. // included in all copies or substantial portions of the Software.
  15. //
  16. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
  17. // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
  18. // OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  19. // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
  20. // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
  21. // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
  22. // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
  23. // OTHER DEALINGS IN THE SOFTWARE.
  24. #endregion
  25. using System;
  26. using System.Collections.Generic;
  27. using System.Diagnostics;
  28. using System.Globalization;
  29. using System.Runtime.Serialization;
  30. #if NET20
  31. using Newtonsoft.Json.Utilities.LinqBridge;
  32. #else
  33. using System.Linq;
  34. #endif
  35. using System.Reflection;
  36. using System.Text;
  37. namespace Newtonsoft.Json.Utilities
  38. {
  39. internal static class EnumUtils
  40. {
  41. private const char EnumSeparatorChar = ',';
  42. private const string EnumSeparatorString = ", ";
  43. private static readonly ThreadSafeStore<Type, EnumInfo> ValuesAndNamesPerEnum = new ThreadSafeStore<Type, EnumInfo>(InitializeValuesAndNames);
  44. private static EnumInfo InitializeValuesAndNames(Type enumType)
  45. {
  46. string[] names = Enum.GetNames(enumType);
  47. string[] resolvedNames = new string[names.Length];
  48. ulong[] values = new ulong[names.Length];
  49. for (int i = 0; i < names.Length; i++)
  50. {
  51. string name = names[i];
  52. FieldInfo f = enumType.GetField(name, BindingFlags.Public | BindingFlags.Static);
  53. values[i] = ToUInt64(f.GetValue(null));
  54. string resolvedName;
  55. #if !NET20
  56. resolvedName = f.GetCustomAttributes(typeof(EnumMemberAttribute), true)
  57. .Cast<EnumMemberAttribute>()
  58. .Select(a => a.Value)
  59. .SingleOrDefault() ?? f.Name;
  60. if (Array.IndexOf(resolvedNames, resolvedName, 0, i) != -1)
  61. {
  62. throw new InvalidOperationException("Enum name '{0}' already exists on enum '{1}'.".FormatWith(CultureInfo.InvariantCulture, resolvedName, enumType.Name));
  63. }
  64. #else
  65. resolvedName = name;
  66. #endif
  67. resolvedNames[i] = resolvedName;
  68. }
  69. bool isFlags = enumType.IsDefined(typeof(FlagsAttribute), false);
  70. return new EnumInfo(isFlags, values, names, resolvedNames);
  71. }
  72. public static IList<T> GetFlagsValues<T>(T value) where T : struct
  73. {
  74. Type enumType = typeof(T);
  75. if (!enumType.IsDefined(typeof(FlagsAttribute), false))
  76. {
  77. throw new ArgumentException("Enum type {0} is not a set of flags.".FormatWith(CultureInfo.InvariantCulture, enumType));
  78. }
  79. Type underlyingType = Enum.GetUnderlyingType(value.GetType());
  80. ulong num = ToUInt64(value);
  81. EnumInfo enumNameValues = GetEnumValuesAndNames(enumType);
  82. IList<T> selectedFlagsValues = new List<T>();
  83. for (int i = 0; i < enumNameValues.Values.Length; i++)
  84. {
  85. ulong v = enumNameValues.Values[i];
  86. if ((num & v) == v && v != 0)
  87. {
  88. selectedFlagsValues.Add((T)Convert.ChangeType(v, underlyingType, CultureInfo.CurrentCulture));
  89. }
  90. }
  91. if (selectedFlagsValues.Count == 0 && enumNameValues.Values.Any(v => v == 0))
  92. {
  93. selectedFlagsValues.Add(default(T));
  94. }
  95. return selectedFlagsValues;
  96. }
  97. public static bool TryToString(Type enumType, object value, bool camelCaseText, out string name)
  98. {
  99. EnumInfo enumInfo = ValuesAndNamesPerEnum.Get(enumType);
  100. ulong v = ToUInt64(value);
  101. if (!enumInfo.IsFlags)
  102. {
  103. int index = Array.BinarySearch(enumInfo.Values, v);
  104. if (index >= 0)
  105. {
  106. name = enumInfo.ResolvedNames[index];
  107. if (camelCaseText)
  108. {
  109. name = StringUtils.ToCamelCase(name);
  110. }
  111. return true;
  112. }
  113. // is number value
  114. name = null;
  115. return false;
  116. }
  117. else // These are flags OR'ed together (We treat everything as unsigned types)
  118. {
  119. name = InternalFlagsFormat(enumInfo, v, camelCaseText);
  120. return name != null;
  121. }
  122. }
  123. private static String InternalFlagsFormat(EnumInfo entry, ulong result, bool camelCaseText)
  124. {
  125. string[] resolvedNames = entry.ResolvedNames;
  126. ulong[] values = entry.Values;
  127. int index = values.Length - 1;
  128. StringBuilder sb = new StringBuilder();
  129. bool firstTime = true;
  130. ulong saveResult = result;
  131. // We will not optimize this code further to keep it maintainable. There are some boundary checks that can be applied
  132. // to minimize the comparsions required. This code works the same for the best/worst case. In general the number of
  133. // items in an enum are sufficiently small and not worth the optimization.
  134. while (index >= 0)
  135. {
  136. if (index == 0 && values[index] == 0)
  137. {
  138. break;
  139. }
  140. if ((result & values[index]) == values[index])
  141. {
  142. result -= values[index];
  143. if (!firstTime)
  144. {
  145. sb.Insert(0, EnumSeparatorString);
  146. }
  147. string resolvedName = resolvedNames[index];
  148. sb.Insert(0, camelCaseText ? StringUtils.ToCamelCase(resolvedName) : resolvedName);
  149. firstTime = false;
  150. }
  151. index--;
  152. }
  153. string returnString;
  154. if (result != 0)
  155. {
  156. // We were unable to represent this number as a bitwise or of valid flags
  157. returnString = null; // return null so the caller knows to .ToString() the input
  158. }
  159. else if (saveResult == 0)
  160. {
  161. // For the cases when we have zero
  162. if (values.Length > 0 && values[0] == 0)
  163. {
  164. returnString = resolvedNames[0]; // Zero was one of the enum values.
  165. if (camelCaseText)
  166. {
  167. returnString = StringUtils.ToCamelCase(returnString);
  168. }
  169. }
  170. else
  171. {
  172. returnString = null;
  173. }
  174. }
  175. else
  176. {
  177. returnString = sb.ToString(); // Return the string representation
  178. }
  179. return returnString;
  180. }
  181. public static EnumInfo GetEnumValuesAndNames(Type enumType)
  182. {
  183. return ValuesAndNamesPerEnum.Get(enumType);
  184. }
  185. private static ulong ToUInt64(object value)
  186. {
  187. PrimitiveTypeCode typeCode = ConvertUtils.GetTypeCode(value.GetType(), out bool _);
  188. switch (typeCode)
  189. {
  190. case PrimitiveTypeCode.SByte:
  191. return (ulong)(sbyte)value;
  192. case PrimitiveTypeCode.Byte:
  193. return (byte)value;
  194. case PrimitiveTypeCode.Boolean:
  195. // direct cast from bool to byte is not allowed
  196. return Convert.ToByte((bool)value);
  197. case PrimitiveTypeCode.Int16:
  198. return (ulong)(short)value;
  199. case PrimitiveTypeCode.UInt16:
  200. return (ushort)value;
  201. case PrimitiveTypeCode.Char:
  202. return (char)value;
  203. case PrimitiveTypeCode.UInt32:
  204. return (uint)value;
  205. case PrimitiveTypeCode.Int32:
  206. return (ulong)(int)value;
  207. case PrimitiveTypeCode.UInt64:
  208. return (ulong)value;
  209. case PrimitiveTypeCode.Int64:
  210. return (ulong)(long)value;
  211. // All unsigned types will be directly cast
  212. default:
  213. throw new InvalidOperationException("Unknown enum type.");
  214. }
  215. }
  216. public static object ParseEnum(Type enumType, string value, bool disallowNumber)
  217. {
  218. ValidationUtils.ArgumentNotNull(enumType, nameof(enumType));
  219. ValidationUtils.ArgumentNotNull(value, nameof(value));
  220. if (!enumType.IsEnum())
  221. {
  222. throw new ArgumentException("Type provided must be an Enum.", nameof(enumType));
  223. }
  224. EnumInfo entry = ValuesAndNamesPerEnum.Get(enumType);
  225. string[] enumNames = entry.Names;
  226. string[] resolvedNames = entry.ResolvedNames;
  227. ulong[] enumValues = entry.Values;
  228. // first check if the entire text (including commas) matches a resolved name
  229. int? matchingIndex = FindIndexByName(resolvedNames, value, 0, value.Length, StringComparison.Ordinal);
  230. if (matchingIndex != null)
  231. {
  232. return Enum.ToObject(enumType, enumValues[matchingIndex.Value]);
  233. }
  234. int firstNonWhitespaceIndex = -1;
  235. for (int i = 0; i < value.Length; i++)
  236. {
  237. if (!char.IsWhiteSpace(value[i]))
  238. {
  239. firstNonWhitespaceIndex = i;
  240. break;
  241. }
  242. }
  243. if (firstNonWhitespaceIndex == -1)
  244. {
  245. throw new ArgumentException("Must specify valid information for parsing in the string.");
  246. }
  247. // check whether string is a number and parse as a number value
  248. char firstNonWhitespaceChar = value[firstNonWhitespaceIndex];
  249. if (char.IsDigit(firstNonWhitespaceChar) || firstNonWhitespaceChar == '-' || firstNonWhitespaceChar == '+')
  250. {
  251. Type underlyingType = Enum.GetUnderlyingType(enumType);
  252. value = value.Trim();
  253. object temp = null;
  254. try
  255. {
  256. temp = Convert.ChangeType(value, underlyingType, CultureInfo.InvariantCulture);
  257. }
  258. catch (FormatException)
  259. {
  260. // We need to Parse this as a String instead. There are cases
  261. // when you tlbimp enums that can have values of the form "3D".
  262. // Don't fix this code.
  263. }
  264. if (temp != null)
  265. {
  266. if (disallowNumber)
  267. {
  268. throw new FormatException("Integer string '{0}' is not allowed.".FormatWith(CultureInfo.InvariantCulture, value));
  269. }
  270. return Enum.ToObject(enumType, temp);
  271. }
  272. }
  273. ulong result = 0;
  274. int valueIndex = firstNonWhitespaceIndex;
  275. while (valueIndex <= value.Length) // '=' is to handle invalid case of an ending comma
  276. {
  277. // Find the next separator, if there is one, otherwise the end of the string.
  278. int endIndex = value.IndexOf(EnumSeparatorChar, valueIndex);
  279. if (endIndex == -1)
  280. {
  281. endIndex = value.Length;
  282. }
  283. // Shift the starting and ending indices to eliminate whitespace
  284. int endIndexNoWhitespace = endIndex;
  285. while (valueIndex < endIndex && char.IsWhiteSpace(value[valueIndex]))
  286. {
  287. valueIndex++;
  288. }
  289. while (endIndexNoWhitespace > valueIndex && char.IsWhiteSpace(value[endIndexNoWhitespace - 1]))
  290. {
  291. endIndexNoWhitespace--;
  292. }
  293. int valueSubstringLength = endIndexNoWhitespace - valueIndex;
  294. // match with case sensitivity
  295. matchingIndex = MatchName(value, enumNames, resolvedNames, valueIndex, valueSubstringLength, StringComparison.Ordinal);
  296. // if no match found, attempt case insensitive search
  297. if (matchingIndex == null)
  298. {
  299. matchingIndex = MatchName(value, enumNames, resolvedNames, valueIndex, valueSubstringLength, StringComparison.OrdinalIgnoreCase);
  300. }
  301. if (matchingIndex == null)
  302. {
  303. // still can't find a match
  304. // before we throw an error, check whether the entire string has a case insensitive match against resolve names
  305. matchingIndex = FindIndexByName(resolvedNames, value, 0, value.Length, StringComparison.OrdinalIgnoreCase);
  306. if (matchingIndex != null)
  307. {
  308. return Enum.ToObject(enumType, enumValues[matchingIndex.Value]);
  309. }
  310. // no match so error
  311. throw new ArgumentException("Requested value '{0}' was not found.".FormatWith(CultureInfo.InvariantCulture, value));
  312. }
  313. result |= enumValues[matchingIndex.Value];
  314. // Move our pointer to the ending index to go again.
  315. valueIndex = endIndex + 1;
  316. }
  317. return Enum.ToObject(enumType, result);
  318. }
  319. private static int? MatchName(string value, string[] enumNames, string[] resolvedNames, int valueIndex, int valueSubstringLength, StringComparison comparison)
  320. {
  321. int? matchingIndex = FindIndexByName(resolvedNames, value, valueIndex, valueSubstringLength, comparison);
  322. if (matchingIndex == null)
  323. {
  324. matchingIndex = FindIndexByName(enumNames, value, valueIndex, valueSubstringLength, comparison);
  325. }
  326. return matchingIndex;
  327. }
  328. private static int? FindIndexByName(string[] enumNames, string value, int valueIndex, int valueSubstringLength, StringComparison comparison)
  329. {
  330. for (int i = 0; i < enumNames.Length; i++)
  331. {
  332. if (enumNames[i].Length == valueSubstringLength &&
  333. string.Compare(enumNames[i], 0, value, valueIndex, valueSubstringLength, comparison) == 0)
  334. {
  335. return i;
  336. }
  337. }
  338. return null;
  339. }
  340. }
  341. }