INI.cs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595
  1. //
  2. // Copyright 2020 Electronic Arts Inc.
  3. //
  4. // The Command & Conquer Map Editor and corresponding source code is free
  5. // software: you can redistribute it and/or modify it under the terms of
  6. // the GNU General Public License as published by the Free Software Foundation,
  7. // either version 3 of the License, or (at your option) any later version.
  8. // The Command & Conquer Map Editor and corresponding source code is distributed
  9. // in the hope that it will be useful, but with permitted additional restrictions
  10. // under Section 7 of the GPL. See the GNU General Public License in LICENSE.TXT
  11. // distributed with this program. You should have received a copy of the
  12. // GNU General Public License along with permitted additional restrictions
  13. // with this program. If not, see https://github.com/electronicarts/CnC_Remastered_Collection
  14. using System;
  15. using System.Collections;
  16. using System.Collections.Generic;
  17. using System.Collections.Specialized;
  18. using System.ComponentModel;
  19. using System.IO;
  20. using System.Linq;
  21. using System.Reflection;
  22. using System.Text;
  23. using System.Text.RegularExpressions;
  24. namespace MobiusEditor.Utility
  25. {
  26. static class INIHelpers
  27. {
  28. public static readonly Regex SectionRegex = new Regex(@"^\s*\[([^\]]*)\]", RegexOptions.Compiled);
  29. public static readonly Regex KeyValueRegex = new Regex(@"^\s*(.*?)\s*=([^;]*)", RegexOptions.Compiled);
  30. public static readonly Regex CommentRegex = new Regex(@"^\s*(#|;)", RegexOptions.Compiled);
  31. public static readonly Func<INIDiffType, string> DiffPrefix = t =>
  32. {
  33. switch (t)
  34. {
  35. case INIDiffType.Added:
  36. return "+";
  37. case INIDiffType.Removed:
  38. return "-";
  39. case INIDiffType.Updated:
  40. return "@";
  41. }
  42. return string.Empty;
  43. };
  44. }
  45. public class INIKeyValueCollection : IEnumerable<(string Key, string Value)>, IEnumerable
  46. {
  47. private readonly OrderedDictionary KeyValues;
  48. public string this[string key]
  49. {
  50. get
  51. {
  52. if (!KeyValues.Contains(key))
  53. {
  54. throw new KeyNotFoundException(key);
  55. }
  56. return KeyValues[key] as string;
  57. }
  58. set
  59. {
  60. if (key == null)
  61. {
  62. throw new ArgumentNullException("key");
  63. }
  64. KeyValues[key] = value;
  65. }
  66. }
  67. public INIKeyValueCollection()
  68. {
  69. KeyValues = new OrderedDictionary(StringComparer.OrdinalIgnoreCase);
  70. }
  71. public int Count => KeyValues.Count;
  72. public bool Contains(string key) => KeyValues.Contains(key);
  73. public T Get<T>(string key) where T : struct
  74. {
  75. var converter = TypeDescriptor.GetConverter(typeof(T));
  76. return (T)converter.ConvertFromString(this[key]);
  77. }
  78. public void Set<T>(string key, T value) where T : struct
  79. {
  80. var converter = TypeDescriptor.GetConverter(typeof(T));
  81. this[key] = converter.ConvertToString(value);
  82. }
  83. public bool Remove(string key)
  84. {
  85. if (!KeyValues.Contains(key))
  86. {
  87. return false;
  88. }
  89. KeyValues.Remove(key);
  90. return true;
  91. }
  92. public IEnumerator<(string Key, string Value)> GetEnumerator()
  93. {
  94. foreach (DictionaryEntry entry in KeyValues)
  95. {
  96. yield return (entry.Key as string, entry.Value as string);
  97. }
  98. }
  99. IEnumerator IEnumerable.GetEnumerator()
  100. {
  101. return GetEnumerator();
  102. }
  103. }
  104. public class INISection : IEnumerable<(string Key, string Value)>, IEnumerable
  105. {
  106. public readonly INIKeyValueCollection Keys;
  107. public string Name { get; private set; }
  108. public string this[string key] { get => Keys[key]; set => Keys[key] = value; }
  109. public bool Empty => Keys.Count == 0;
  110. public INISection(string name)
  111. {
  112. Keys = new INIKeyValueCollection();
  113. Name = name;
  114. }
  115. public void Parse(TextReader reader)
  116. {
  117. while (true)
  118. {
  119. var line = reader.ReadLine();
  120. if (line == null)
  121. {
  122. break;
  123. }
  124. var m = INIHelpers.KeyValueRegex.Match(line);
  125. if (m.Success)
  126. {
  127. Keys[m.Groups[1].Value] = m.Groups[2].Value;
  128. }
  129. }
  130. }
  131. public void Parse(string iniText)
  132. {
  133. using (var reader = new StringReader(iniText))
  134. {
  135. Parse(reader);
  136. }
  137. }
  138. public IEnumerator<(string Key, string Value)> GetEnumerator()
  139. {
  140. return Keys.GetEnumerator();
  141. }
  142. IEnumerator IEnumerable.GetEnumerator()
  143. {
  144. return GetEnumerator();
  145. }
  146. public override string ToString()
  147. {
  148. var lines = new List<string>(Keys.Count);
  149. foreach (var item in Keys)
  150. {
  151. lines.Add(string.Format("{0}={1}", item.Key, item.Value));
  152. }
  153. return string.Join(Environment.NewLine, lines);
  154. }
  155. }
  156. public class INISectionCollection : IEnumerable<INISection>, IEnumerable
  157. {
  158. private readonly OrderedDictionary Sections;
  159. public INISection this[string name] => Sections.Contains(name) ? (Sections[name] as INISection) : null;
  160. public INISectionCollection()
  161. {
  162. Sections = new OrderedDictionary(StringComparer.OrdinalIgnoreCase);
  163. }
  164. public int Count => Sections.Count;
  165. public bool Contains(string section) => Sections.Contains(section);
  166. public INISection Add(string name)
  167. {
  168. if (!Sections.Contains(name))
  169. {
  170. var section = new INISection(name);
  171. Sections[name] = section;
  172. }
  173. return this[name];
  174. }
  175. public bool Add(INISection section)
  176. {
  177. if ((section == null) || Sections.Contains(section.Name))
  178. {
  179. return false;
  180. }
  181. Sections[section.Name] = section;
  182. return true;
  183. }
  184. public void AddRange(IEnumerable<INISection> sections)
  185. {
  186. foreach (var section in sections)
  187. {
  188. Add(section);
  189. }
  190. }
  191. public bool Remove(string name)
  192. {
  193. if (!Sections.Contains(name))
  194. {
  195. return false;
  196. }
  197. Sections.Remove(name);
  198. return true;
  199. }
  200. public INISection Extract(string name)
  201. {
  202. if (!Sections.Contains(name))
  203. {
  204. return null;
  205. }
  206. var section = this[name];
  207. Sections.Remove(name);
  208. return section;
  209. }
  210. public IEnumerator<INISection> GetEnumerator()
  211. {
  212. foreach (DictionaryEntry entry in Sections)
  213. {
  214. yield return entry.Value as INISection;
  215. }
  216. }
  217. IEnumerator IEnumerable.GetEnumerator()
  218. {
  219. return GetEnumerator();
  220. }
  221. }
  222. public partial class INI : IEnumerable<INISection>, IEnumerable
  223. {
  224. public readonly INISectionCollection Sections;
  225. public INISection this[string name] { get => Sections[name]; }
  226. public INI()
  227. {
  228. Sections = new INISectionCollection();
  229. }
  230. public void Parse(TextReader reader)
  231. {
  232. INISection currentSection = null;
  233. while (true)
  234. {
  235. var line = reader.ReadLine();
  236. if (line == null)
  237. {
  238. break;
  239. }
  240. var m = INIHelpers.SectionRegex.Match(line);
  241. if (m.Success)
  242. {
  243. currentSection = Sections.Add(m.Groups[1].Value);
  244. }
  245. if (currentSection != null)
  246. {
  247. if (INIHelpers.CommentRegex.Match(line).Success)
  248. {
  249. continue;
  250. }
  251. currentSection.Parse(line);
  252. }
  253. }
  254. }
  255. public void Parse(string iniText)
  256. {
  257. using (var reader = new StringReader(iniText))
  258. {
  259. Parse(reader);
  260. }
  261. }
  262. public IEnumerator<INISection> GetEnumerator()
  263. {
  264. foreach (var section in Sections)
  265. {
  266. yield return section;
  267. }
  268. }
  269. IEnumerator IEnumerable.GetEnumerator()
  270. {
  271. return GetEnumerator();
  272. }
  273. public override string ToString()
  274. {
  275. var sections = new List<string>(Sections.Count);
  276. foreach (var item in Sections)
  277. {
  278. var lines = new List<string>
  279. {
  280. string.Format("[{0}]", item.Name)
  281. };
  282. if (!item.Empty)
  283. {
  284. lines.Add(item.ToString());
  285. }
  286. sections.Add(string.Join(Environment.NewLine, lines));
  287. }
  288. return string.Join(Environment.NewLine + Environment.NewLine, sections) + Environment.NewLine;
  289. }
  290. }
  291. [Flags]
  292. public enum INIDiffType
  293. {
  294. None = 0,
  295. Added = 1,
  296. Removed = 2,
  297. Updated = 4,
  298. AddedOrUpdated = 5
  299. }
  300. public class INISectionDiff : IEnumerable<string>, IEnumerable
  301. {
  302. public readonly INIDiffType Type;
  303. private readonly Dictionary<string, INIDiffType> keyDiff;
  304. public INIDiffType this[string key]
  305. {
  306. get
  307. {
  308. INIDiffType diffType;
  309. if (!keyDiff.TryGetValue(key, out diffType))
  310. {
  311. return INIDiffType.None;
  312. }
  313. return diffType;
  314. }
  315. }
  316. private INISectionDiff()
  317. {
  318. keyDiff = new Dictionary<string, INIDiffType>();
  319. Type = INIDiffType.None;
  320. }
  321. internal INISectionDiff(INIDiffType type, INISection section)
  322. : this()
  323. {
  324. foreach (var keyValue in section.Keys)
  325. {
  326. keyDiff[keyValue.Key] = type;
  327. }
  328. Type = type;
  329. }
  330. internal INISectionDiff(INISection leftSection, INISection rightSection)
  331. : this(INIDiffType.Removed, leftSection)
  332. {
  333. foreach (var keyValue in rightSection.Keys)
  334. {
  335. var key = keyValue.Key;
  336. if (keyDiff.ContainsKey(key))
  337. {
  338. if (leftSection[key] == rightSection[key])
  339. {
  340. keyDiff.Remove(key);
  341. }
  342. else
  343. {
  344. keyDiff[key] = INIDiffType.Updated;
  345. Type = INIDiffType.Updated;
  346. }
  347. }
  348. else
  349. {
  350. keyDiff[key] = INIDiffType.Added;
  351. Type = INIDiffType.Updated;
  352. }
  353. }
  354. Type = (keyDiff.Count > 0) ? INIDiffType.Updated : INIDiffType.None;
  355. }
  356. public IEnumerator<string> GetEnumerator()
  357. {
  358. return keyDiff.Keys.GetEnumerator();
  359. }
  360. IEnumerator IEnumerable.GetEnumerator()
  361. {
  362. return GetEnumerator();
  363. }
  364. public override string ToString()
  365. {
  366. var sb = new StringBuilder();
  367. foreach (var item in keyDiff)
  368. {
  369. sb.AppendLine(string.Format("{0} {1}", INIHelpers.DiffPrefix(item.Value), item.Key));
  370. }
  371. return sb.ToString();
  372. }
  373. }
  374. public class INIDiff : IEnumerable<string>, IEnumerable
  375. {
  376. private readonly Dictionary<string, INISectionDiff> sectionDiffs;
  377. public INISectionDiff this[string key]
  378. {
  379. get
  380. {
  381. if (!sectionDiffs.TryGetValue(key, out INISectionDiff sectionDiff))
  382. {
  383. return null;
  384. }
  385. return sectionDiff;
  386. }
  387. }
  388. private INIDiff()
  389. {
  390. sectionDiffs = new Dictionary<string, INISectionDiff>(StringComparer.OrdinalIgnoreCase);
  391. }
  392. public INIDiff(INI leftIni, INI rightIni)
  393. : this()
  394. {
  395. foreach (var leftSection in leftIni)
  396. {
  397. sectionDiffs[leftSection.Name] = rightIni.Sections.Contains(leftSection.Name) ?
  398. new INISectionDiff(leftSection, rightIni[leftSection.Name]) :
  399. new INISectionDiff(INIDiffType.Removed, leftSection);
  400. }
  401. foreach (var rightSection in rightIni)
  402. {
  403. if (!leftIni.Sections.Contains(rightSection.Name))
  404. {
  405. sectionDiffs[rightSection.Name] = new INISectionDiff(INIDiffType.Added, rightSection);
  406. }
  407. }
  408. sectionDiffs = sectionDiffs.Where(x => x.Value.Type != INIDiffType.None).ToDictionary(x => x.Key, x => x.Value);
  409. }
  410. public bool Contains(string key) => sectionDiffs.ContainsKey(key);
  411. public IEnumerator<string> GetEnumerator()
  412. {
  413. return sectionDiffs.Keys.GetEnumerator();
  414. }
  415. IEnumerator IEnumerable.GetEnumerator()
  416. {
  417. return GetEnumerator();
  418. }
  419. public override string ToString()
  420. {
  421. var sb = new StringBuilder();
  422. foreach (var item in sectionDiffs)
  423. {
  424. sb.AppendLine(string.Format("{0} {1}", INIHelpers.DiffPrefix(item.Value.Type), item.Key));
  425. using (var reader = new StringReader(item.Value.ToString()))
  426. {
  427. while (true)
  428. {
  429. var line = reader.ReadLine();
  430. if (line == null)
  431. {
  432. break;
  433. }
  434. sb.AppendLine(string.Format("\t{0}", line));
  435. }
  436. }
  437. }
  438. return sb.ToString();
  439. }
  440. }
  441. [AttributeUsage(AttributeTargets.Property)]
  442. public class NonSerializedINIKeyAttribute : Attribute
  443. {
  444. }
  445. public partial class INI
  446. {
  447. public static void ParseSection<T>(ITypeDescriptorContext context, INISection section, T data)
  448. {
  449. var propertyDescriptors = TypeDescriptor.GetProperties(data);
  450. var properties = data.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(p => p.GetSetMethod() != null);
  451. foreach (var property in properties)
  452. {
  453. if (property.GetCustomAttribute<NonSerializedINIKeyAttribute>() != null)
  454. {
  455. continue;
  456. }
  457. if (section.Keys.Contains(property.Name))
  458. {
  459. var converter = propertyDescriptors.Find(property.Name, false)?.Converter ?? TypeDescriptor.GetConverter(property.PropertyType);
  460. if (converter.CanConvertFrom(context, typeof(string)))
  461. {
  462. try
  463. {
  464. property.SetValue(data, converter.ConvertFromString(context, section[property.Name]));
  465. }
  466. catch (FormatException)
  467. {
  468. if (property.PropertyType == typeof(bool))
  469. {
  470. var value = section[property.Name].ToLower();
  471. if (value == "no")
  472. {
  473. property.SetValue(data, false);
  474. }
  475. else if (value == "yes")
  476. {
  477. property.SetValue(data, true);
  478. }
  479. else
  480. {
  481. throw;
  482. }
  483. }
  484. else
  485. {
  486. throw;
  487. }
  488. }
  489. }
  490. }
  491. }
  492. }
  493. public static void WriteSection<T>(ITypeDescriptorContext context, INISection section, T data)
  494. {
  495. var propertyDescriptors = TypeDescriptor.GetProperties(data);
  496. var properties = data.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(p => p.GetGetMethod() != null);
  497. foreach (var property in properties)
  498. {
  499. if (property.GetCustomAttribute<NonSerializedINIKeyAttribute>() != null)
  500. {
  501. continue;
  502. }
  503. var value = property.GetValue(data);
  504. if (property.PropertyType.IsValueType || (value != null))
  505. {
  506. var converter = propertyDescriptors.Find(property.Name, false)?.Converter ?? TypeDescriptor.GetConverter(property.PropertyType);
  507. if (converter.CanConvertTo(context, typeof(string)))
  508. {
  509. section[property.Name] = converter.ConvertToString(context, value);
  510. }
  511. }
  512. }
  513. }
  514. public static void ParseSection<T>(INISection section, T data) => ParseSection(null, section, data);
  515. public static void WriteSection<T>(INISection section, T data) => WriteSection(null, section, data);
  516. }
  517. }