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