mirror of
https://github.com/claunia/SabreTools.git
synced 2025-12-16 19:14:27 +00:00
Add and use IniReader, fix RC again
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
using SabreTools.Library.Data;
|
||||
using SabreTools.Library.DatItems;
|
||||
using SabreTools.Library.Readers;
|
||||
using SabreTools.Library.Tools;
|
||||
using NaturalSort;
|
||||
|
||||
@@ -45,48 +45,286 @@ namespace SabreTools.Library.DatFiles
|
||||
bool clean,
|
||||
bool remUnicode)
|
||||
{
|
||||
// Outsource the work of parsing the file to a helper
|
||||
IniFile ini = new IniFile(filename);
|
||||
// Prepare all intenral variables
|
||||
Encoding enc = Utilities.GetEncoding(filename);
|
||||
IniReader ir = Utilities.GetIniReader(filename, false);
|
||||
|
||||
// CREDITS section
|
||||
Author = string.IsNullOrWhiteSpace(Author) ? ini["CREDITS.author"] : Author;
|
||||
Version = string.IsNullOrWhiteSpace(Version) ? ini["CREDITS.version"] : Version;
|
||||
Email = string.IsNullOrWhiteSpace(Email) ? ini["CREDITS.email"] : Email;
|
||||
Homepage = string.IsNullOrWhiteSpace(Homepage) ? ini["CREDITS.homepage"] : Homepage;
|
||||
Url = string.IsNullOrWhiteSpace(Url) ? ini["CREDITS.url"] : Url;
|
||||
Date = string.IsNullOrWhiteSpace(Date) ? ini["CREDITS.date"] : Date;
|
||||
// If we got a null reader, just return
|
||||
if (ir == null)
|
||||
return;
|
||||
|
||||
// DAT section
|
||||
//RCVersion = string.IsNullOrWhiteSpace(RCVersion) ? ini["CREDITS.version"] : RCVersion;
|
||||
//Plugin = string.IsNullOrWhiteSpace(Plugin) ? ini["CREDITS.plugin"] : Plugin;
|
||||
if (ForceMerging == ForceMerging.None)
|
||||
// Otherwise, read teh file to the end
|
||||
try
|
||||
{
|
||||
if (ini["DAT.split"] == "1")
|
||||
ForceMerging = ForceMerging.Split;
|
||||
else if (ini["DAT.merge"] == "1")
|
||||
ForceMerging = ForceMerging.Merged;
|
||||
ir.ReadNextLine();
|
||||
while (!ir.EndOfStream)
|
||||
{
|
||||
// We don't care about whitespace or comments
|
||||
if (ir.RowType == IniRowType.None || ir.RowType == IniRowType.Comment)
|
||||
{
|
||||
ir.ReadNextLine();
|
||||
continue;
|
||||
}
|
||||
|
||||
// EMULATOR section
|
||||
Name = string.IsNullOrWhiteSpace(Name) ? ini["EMULATOR.refname"] : Name;
|
||||
Description = string.IsNullOrWhiteSpace(Description) ? ini["EMULATOR.version"] : Description;
|
||||
|
||||
// GAMES section
|
||||
foreach (string game in ini.Where(kvp => kvp.Value == null).Select(kvp => kvp.Key))
|
||||
// If we have a section
|
||||
if (ir.RowType == IniRowType.SectionHeader)
|
||||
{
|
||||
// Get the line into a separate variable so it can be manipulated
|
||||
string line = game;
|
||||
|
||||
// Remove INI prefixing
|
||||
if (line.StartsWith("GAMES"))
|
||||
line = line.Substring("GAMES.".Length);
|
||||
|
||||
// If we have a valid game
|
||||
if (line.StartsWith("¬"))
|
||||
switch (ir.Section.ToLowerInvariant())
|
||||
{
|
||||
case "credits":
|
||||
ReadCreditsSection(ir);
|
||||
break;
|
||||
|
||||
case "dat":
|
||||
ReadDatSection(ir);
|
||||
break;
|
||||
|
||||
case "emulator":
|
||||
ReadEmulatorSection(ir);
|
||||
break;
|
||||
|
||||
case "games":
|
||||
ReadGamesSection(ir, sysid, srcid, clean, remUnicode);
|
||||
break;
|
||||
|
||||
// Unknown section so we ignore it
|
||||
default:
|
||||
ir.ReadNextLine();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Globals.Logger.Warning($"Exception found while parsing '{filename}': {ex}");
|
||||
}
|
||||
|
||||
ir.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read credits information
|
||||
/// </summary>
|
||||
/// <param name="reader">IniReader to use to parse the credits</param>
|
||||
private void ReadCreditsSection(IniReader reader)
|
||||
{
|
||||
// If the reader is somehow null, skip it
|
||||
if (reader == null)
|
||||
return;
|
||||
|
||||
reader.ReadNextLine();
|
||||
while (!reader.EndOfStream && reader.Section.ToLowerInvariant() == "credits")
|
||||
{
|
||||
// We don't care about whitespace, comments, or invalid
|
||||
if (reader.RowType != IniRowType.KeyValue)
|
||||
{
|
||||
reader.ReadNextLine();
|
||||
continue;
|
||||
}
|
||||
|
||||
var kvp = reader.KeyValuePair;
|
||||
|
||||
// If the KeyValuePair is invalid, skip it
|
||||
if (kvp == null)
|
||||
{
|
||||
reader.ReadNextLine();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get all credits items (ONLY OVERWRITE IF THERE'S NO DATA)
|
||||
switch (kvp?.Key.ToLowerInvariant())
|
||||
{
|
||||
case "author":
|
||||
Author = string.IsNullOrWhiteSpace(Author) ? kvp?.Value : Author;
|
||||
reader.ReadNextLine();
|
||||
break;
|
||||
|
||||
case "version":
|
||||
Version = string.IsNullOrWhiteSpace(Version) ? kvp?.Value : Version;
|
||||
reader.ReadNextLine();
|
||||
break;
|
||||
|
||||
case "email":
|
||||
Email = string.IsNullOrWhiteSpace(Email) ? kvp?.Value : Email;
|
||||
reader.ReadNextLine();
|
||||
break;
|
||||
|
||||
case "homepage":
|
||||
Homepage = string.IsNullOrWhiteSpace(Homepage) ? kvp?.Value : Homepage;
|
||||
reader.ReadNextLine();
|
||||
break;
|
||||
|
||||
case "url":
|
||||
Url = string.IsNullOrWhiteSpace(Url) ? kvp?.Value : Url;
|
||||
reader.ReadNextLine();
|
||||
break;
|
||||
|
||||
case "date":
|
||||
Date = string.IsNullOrWhiteSpace(Date) ? kvp?.Value : Date;
|
||||
reader.ReadNextLine();
|
||||
break;
|
||||
|
||||
// Unknown value, just skip
|
||||
default:
|
||||
reader.ReadNextLine();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read dat information
|
||||
/// </summary>
|
||||
/// <param name="reader">IniReader to use to parse the credits</param>
|
||||
private void ReadDatSection(IniReader reader)
|
||||
{
|
||||
// If the reader is somehow null, skip it
|
||||
if (reader == null)
|
||||
return;
|
||||
|
||||
reader.ReadNextLine();
|
||||
while (!reader.EndOfStream && reader.Section.ToLowerInvariant() == "dat")
|
||||
{
|
||||
// We don't care about whitespace, comments, or invalid
|
||||
if (reader.RowType != IniRowType.KeyValue)
|
||||
{
|
||||
reader.ReadNextLine();
|
||||
continue;
|
||||
}
|
||||
|
||||
var kvp = reader.KeyValuePair;
|
||||
|
||||
// If the KeyValuePair is invalid, skip it
|
||||
if (kvp == null)
|
||||
{
|
||||
reader.ReadNextLine();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get all dat items (ONLY OVERWRITE IF THERE'S NO DATA)
|
||||
switch (kvp?.Key.ToLowerInvariant())
|
||||
{
|
||||
case "version":
|
||||
string rcVersion = kvp?.Value;
|
||||
reader.ReadNextLine();
|
||||
break;
|
||||
|
||||
case "plugin":
|
||||
string plugin = kvp?.Value;
|
||||
reader.ReadNextLine();
|
||||
break;
|
||||
|
||||
case "split":
|
||||
if (ForceMerging == ForceMerging.None && kvp?.Value == "1")
|
||||
ForceMerging = ForceMerging.Split;
|
||||
|
||||
reader.ReadNextLine();
|
||||
break;
|
||||
|
||||
case "merge":
|
||||
if (ForceMerging == ForceMerging.None && kvp?.Value == "1")
|
||||
ForceMerging = ForceMerging.Merged;
|
||||
|
||||
reader.ReadNextLine();
|
||||
break;
|
||||
|
||||
// Unknown value, just skip
|
||||
default:
|
||||
reader.ReadNextLine();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read emulator information
|
||||
/// </summary>
|
||||
/// <param name="reader">IniReader to use to parse the credits</param>
|
||||
private void ReadEmulatorSection(IniReader reader)
|
||||
{
|
||||
// If the reader is somehow null, skip it
|
||||
if (reader == null)
|
||||
return;
|
||||
|
||||
reader.ReadNextLine();
|
||||
while (!reader.EndOfStream && reader.Section.ToLowerInvariant() == "emulator")
|
||||
{
|
||||
// We don't care about whitespace, comments, or invalid
|
||||
if (reader.RowType != IniRowType.KeyValue)
|
||||
{
|
||||
reader.ReadNextLine();
|
||||
continue;
|
||||
}
|
||||
|
||||
var kvp = reader.KeyValuePair;
|
||||
|
||||
// If the KeyValuePair is invalid, skip it
|
||||
if (kvp == null)
|
||||
{
|
||||
reader.ReadNextLine();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get all emulator items (ONLY OVERWRITE IF THERE'S NO DATA)
|
||||
switch (kvp?.Key.ToLowerInvariant())
|
||||
{
|
||||
case "refname":
|
||||
Name = string.IsNullOrWhiteSpace(Name) ? kvp?.Value : Name;
|
||||
reader.ReadNextLine();
|
||||
break;
|
||||
|
||||
case "version":
|
||||
Description = string.IsNullOrWhiteSpace(Description) ? kvp?.Value : Description;
|
||||
reader.ReadNextLine();
|
||||
break;
|
||||
|
||||
// Unknown value, just skip
|
||||
default:
|
||||
reader.ReadNextLine();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read games information
|
||||
/// </summary>
|
||||
/// <param name="reader">IniReader to use to parse the credits</param>
|
||||
/// <param name="sysid">System ID for the DAT</param>
|
||||
/// <param name="srcid">Source ID for the DAT</param>
|
||||
/// <param name="clean">True if game names are sanitized, false otherwise (default)</param>
|
||||
/// <param name="remUnicode">True if we should remove non-ASCII characters from output, false otherwise (default)</param>
|
||||
private void ReadGamesSection(IniReader reader, int sysid, int srcid, bool clean, bool remUnicode)
|
||||
{
|
||||
// If the reader is somehow null, skip it
|
||||
if (reader == null)
|
||||
return;
|
||||
|
||||
reader.ReadNextLine();
|
||||
while (!reader.EndOfStream && reader.Section.ToLowerInvariant() == "games")
|
||||
{
|
||||
// We don't care about whitespace or comments
|
||||
// We're keeping keyvalue in case a file has '=' in the row
|
||||
if (reader.RowType != IniRowType.Invalid && reader.RowType != IniRowType.KeyValue)
|
||||
{
|
||||
reader.ReadNextLine();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Roms are not valid row formats, usually
|
||||
string line = reader.Line;
|
||||
|
||||
// If we don't have a valid game, keep reading
|
||||
if (!line.StartsWith("¬"))
|
||||
{
|
||||
reader.ReadNextLine();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Some old RC DATs have this behavior
|
||||
if (line.Contains("¬N¬O"))
|
||||
line = game.Replace("¬N¬O", string.Empty) + "¬¬";
|
||||
line = line.Replace("¬N¬O", string.Empty) + "¬¬";
|
||||
|
||||
/*
|
||||
The rominfo order is as follows:
|
||||
@@ -124,7 +362,8 @@ namespace SabreTools.Library.DatFiles
|
||||
|
||||
// Now process and add the rom
|
||||
ParseAddHelper(rom, clean, remUnicode);
|
||||
}
|
||||
|
||||
reader.ReadNextLine();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,38 +503,27 @@ namespace SabreTools.Library.DatFiles
|
||||
if (ignoreblanks && (datItem.ItemType == ItemType.Rom && ((datItem as Rom).Size == 0 || (datItem as Rom).Size == -1)))
|
||||
return true;
|
||||
|
||||
/*
|
||||
The rominfo order is as follows:
|
||||
1 - parent name
|
||||
2 - parent description
|
||||
3 - game name
|
||||
4 - game description
|
||||
5 - rom name
|
||||
6 - rom crc
|
||||
7 - rom size
|
||||
8 - romof name
|
||||
9 - merge name
|
||||
*/
|
||||
|
||||
try
|
||||
{
|
||||
// Pre-process the item name
|
||||
ProcessItemName(datItem, true);
|
||||
|
||||
// Build the state based on excluded fields
|
||||
switch (datItem.ItemType)
|
||||
{
|
||||
case ItemType.Disk:
|
||||
sw.Write("¬");
|
||||
if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.CloneOf, ExcludeFields)))
|
||||
sw.Write(datItem.CloneOf);
|
||||
sw.Write("¬");
|
||||
if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.CloneOf, ExcludeFields)))
|
||||
sw.Write(datItem.CloneOf);
|
||||
sw.Write($"¬{datItem.GetField(Field.MachineName, ExcludeFields)}");
|
||||
if (string.IsNullOrWhiteSpace(datItem.MachineDescription))
|
||||
sw.Write($"¬{datItem.GetField(Field.MachineName, ExcludeFields)}");
|
||||
else
|
||||
sw.Write($"¬{datItem.GetField(Field.Description, ExcludeFields)}");
|
||||
sw.Write($"¬{datItem.GetField(Field.Name, ExcludeFields)}");
|
||||
sw.Write("¬¬¬¬¬\n");
|
||||
break;
|
||||
|
||||
case ItemType.Rom:
|
||||
var rom = datItem as Rom;
|
||||
sw.Write("¬");
|
||||
if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.CloneOf, ExcludeFields)))
|
||||
sw.Write(datItem.CloneOf);
|
||||
sw.Write("¬");
|
||||
if (!string.IsNullOrWhiteSpace(datItem.GetField(Field.CloneOf, ExcludeFields)))
|
||||
sw.Write(datItem.CloneOf);
|
||||
sw.Write($"¬{datItem.GetField(Field.CloneOf, ExcludeFields)}");
|
||||
sw.Write($"¬{datItem.GetField(Field.CloneOf, ExcludeFields)}");
|
||||
sw.Write($"¬{datItem.GetField(Field.MachineName, ExcludeFields)}");
|
||||
if (string.IsNullOrWhiteSpace(datItem.MachineDescription))
|
||||
sw.Write($"¬{datItem.GetField(Field.MachineName, ExcludeFields)}");
|
||||
@@ -304,9 +532,9 @@ namespace SabreTools.Library.DatFiles
|
||||
sw.Write($"¬{datItem.GetField(Field.Name, ExcludeFields)}");
|
||||
sw.Write($"¬{datItem.GetField(Field.CRC, ExcludeFields)}");
|
||||
sw.Write($"¬{datItem.GetField(Field.Size, ExcludeFields)}");
|
||||
sw.Write("¬¬¬\n");
|
||||
break;
|
||||
}
|
||||
sw.Write($"¬{datItem.GetField(Field.RomOf, ExcludeFields)}");
|
||||
sw.Write($"¬{datItem.GetField(Field.Merge, ExcludeFields)}");
|
||||
sw.Write("¬\n");
|
||||
|
||||
sw.Flush();
|
||||
}
|
||||
|
||||
@@ -438,6 +438,22 @@
|
||||
|
||||
#endregion
|
||||
|
||||
#region Reader related
|
||||
|
||||
/// <summary>
|
||||
/// Different types of INI rows being parsed
|
||||
/// </summary>
|
||||
public enum IniRowType
|
||||
{
|
||||
None,
|
||||
SectionHeader,
|
||||
KeyValue,
|
||||
Comment,
|
||||
Invalid,
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Skippers and Mappers
|
||||
|
||||
/// <summary>
|
||||
|
||||
144
SabreTools.Library/Readers/IniReader.cs
Normal file
144
SabreTools.Library/Readers/IniReader.cs
Normal file
@@ -0,0 +1,144 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
using SabreTools.Library.Data;
|
||||
|
||||
namespace SabreTools.Library.Readers
|
||||
{
|
||||
public class IniReader : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Internal stream reader for inputting
|
||||
/// </summary>
|
||||
private StreamReader sr;
|
||||
|
||||
/// <summary>
|
||||
/// Get if at end of stream
|
||||
/// </summary>
|
||||
public bool EndOfStream
|
||||
{
|
||||
get
|
||||
{
|
||||
return sr?.EndOfStream ?? true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Contents of the currently read line as a key value pair
|
||||
/// </summary>
|
||||
public KeyValuePair<string, string>? KeyValuePair { get; private set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Contents of the currently read line
|
||||
/// </summary>
|
||||
public string Line { get; private set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Current row type
|
||||
/// </summary>
|
||||
public IniRowType RowType { get; private set; } = IniRowType.None;
|
||||
|
||||
/// <summary>
|
||||
/// Current section being read
|
||||
/// </summary>
|
||||
public string Section { get; private set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Validate that rows are in key=value format
|
||||
/// </summary>
|
||||
public bool ValidateRows { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for reading from a file
|
||||
/// </summary>
|
||||
public IniReader(string filename)
|
||||
{
|
||||
sr = new StreamReader(filename);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for reading from a stream
|
||||
/// </summary>
|
||||
public IniReader(Stream stream, Encoding encoding)
|
||||
{
|
||||
sr = new StreamReader(stream, encoding);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read the next line in the INI file
|
||||
/// </summary>
|
||||
public bool ReadNextLine()
|
||||
{
|
||||
if (!(sr.BaseStream?.CanRead ?? false) || sr.EndOfStream)
|
||||
return false;
|
||||
|
||||
Line = sr.ReadLine().Trim();
|
||||
ProcessLine();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process the current line and extract out values
|
||||
/// </summary>
|
||||
private void ProcessLine()
|
||||
{
|
||||
// Comment
|
||||
if (Line.StartsWith(";"))
|
||||
{
|
||||
KeyValuePair = null;
|
||||
RowType = IniRowType.Comment;
|
||||
}
|
||||
|
||||
// Section
|
||||
else if (Line.StartsWith("[") && Line.EndsWith("]"))
|
||||
{
|
||||
KeyValuePair = null;
|
||||
RowType = IniRowType.SectionHeader;
|
||||
Section = Line.TrimStart('[').TrimEnd(']');
|
||||
}
|
||||
|
||||
// KeyValuePair
|
||||
else if (Line.Contains("="))
|
||||
{
|
||||
// Split the line by '=' for key-value pairs
|
||||
string[] data = Line.Split('=');
|
||||
|
||||
// If the value field contains an '=', we need to put them back in
|
||||
string key = data[0].Trim();
|
||||
string value = string.Join("=", data.Skip(1)).Trim();
|
||||
|
||||
KeyValuePair = new KeyValuePair<string, string>(key, value);
|
||||
RowType = IniRowType.KeyValue;
|
||||
}
|
||||
|
||||
// Empty
|
||||
else if (string.IsNullOrEmpty(Line))
|
||||
{
|
||||
KeyValuePair = null;
|
||||
Line = string.Empty;
|
||||
RowType = IniRowType.None;
|
||||
}
|
||||
|
||||
// Invalid
|
||||
else
|
||||
{
|
||||
KeyValuePair = null;
|
||||
RowType = IniRowType.Invalid;
|
||||
|
||||
if (ValidateRows)
|
||||
throw new InvalidDataException($"Invalid INI row found, cannot continue: {Line}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose of the reader
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
sr.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,305 +0,0 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace SabreTools.Library.Tools
|
||||
{
|
||||
public class IniFile : IDictionary<string, string>
|
||||
{
|
||||
private Dictionary<string, string> _keyValuePairs = new Dictionary<string, string>();
|
||||
|
||||
public string this[string key]
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_keyValuePairs == null)
|
||||
_keyValuePairs = new Dictionary<string, string>();
|
||||
|
||||
key = key.ToLowerInvariant();
|
||||
if (_keyValuePairs.ContainsKey(key))
|
||||
return _keyValuePairs[key];
|
||||
|
||||
return null;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (_keyValuePairs == null)
|
||||
_keyValuePairs = new Dictionary<string, string>();
|
||||
|
||||
key = key.ToLowerInvariant();
|
||||
_keyValuePairs[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create an empty INI file
|
||||
/// </summary>
|
||||
public IniFile()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Populate an INI file from path
|
||||
/// </summary>
|
||||
public IniFile(string path)
|
||||
{
|
||||
this.Parse(path);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Populate an INI file from stream
|
||||
/// </summary>
|
||||
public IniFile(Stream stream)
|
||||
{
|
||||
this.Parse(stream);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add or update a key and value to the INI file
|
||||
/// </summary>
|
||||
public void AddOrUpdate(string key, string value)
|
||||
{
|
||||
_keyValuePairs[key.ToLowerInvariant()] = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove a key from the INI file
|
||||
/// </summary>
|
||||
public void Remove(string key)
|
||||
{
|
||||
_keyValuePairs.Remove(key.ToLowerInvariant());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read an INI file based on the path
|
||||
/// </summary>
|
||||
public bool Parse(string path)
|
||||
{
|
||||
// If we don't have a file, we can't read it
|
||||
if (!File.Exists(path))
|
||||
return false;
|
||||
|
||||
using (var fileStream = File.OpenRead(path))
|
||||
{
|
||||
return Parse(fileStream);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read an INI file from a stream
|
||||
/// </summary>
|
||||
public bool Parse(Stream stream)
|
||||
{
|
||||
// If the stream is invalid or unreadable, we can't process it
|
||||
if (stream == null || !stream.CanRead || stream.Position >= stream.Length - 1)
|
||||
return false;
|
||||
|
||||
// Keys are case-insensitive by default
|
||||
try
|
||||
{
|
||||
using (StreamReader sr = new StreamReader(stream))
|
||||
{
|
||||
string section = string.Empty;
|
||||
while (!sr.EndOfStream)
|
||||
{
|
||||
string line = sr.ReadLine().Trim();
|
||||
|
||||
// Comments start with ';'
|
||||
if (line.StartsWith(";"))
|
||||
{
|
||||
// No-op, we don't process comments
|
||||
}
|
||||
|
||||
// Section titles are surrounded by square brackets
|
||||
else if (line.StartsWith("["))
|
||||
{
|
||||
section = line.TrimStart('[').TrimEnd(']');
|
||||
}
|
||||
|
||||
// Valid INI lines are in the format key=value
|
||||
else if (line.Contains("="))
|
||||
{
|
||||
// Split the line by '=' for key-value pairs
|
||||
string[] data = line.Split('=');
|
||||
|
||||
// If the value field contains an '=', we need to put them back in
|
||||
string key = data[0].Trim();
|
||||
string value = string.Join("=", data.Skip(1)).Trim();
|
||||
|
||||
// Section names are prepended to the key with a '.' separating
|
||||
if (!string.IsNullOrEmpty(section))
|
||||
key = $"{section}.{key}";
|
||||
|
||||
// Set or overwrite keys in the returned dictionary
|
||||
_keyValuePairs[key.ToLowerInvariant()] = value;
|
||||
}
|
||||
|
||||
// Lines that aren't a section or key=value are assumed to be key=null
|
||||
else
|
||||
{
|
||||
// Section names are prepended to the key with a '.' separating
|
||||
if (!string.IsNullOrEmpty(section))
|
||||
line = $"{section}.{line}";
|
||||
|
||||
// Note that these items are NOT normalized
|
||||
_keyValuePairs[line] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// We don't care what the error was, just catch and return
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write an INI file to a path
|
||||
/// </summary>
|
||||
public bool Write(string path)
|
||||
{
|
||||
// If we don't have a valid dictionary with values, we can't write out
|
||||
if (_keyValuePairs == null || _keyValuePairs.Count == 0)
|
||||
return false;
|
||||
|
||||
using (var fileStream = File.OpenWrite(path))
|
||||
{
|
||||
return Write(fileStream);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write an INI file to a stream
|
||||
/// </summary>
|
||||
public bool Write(Stream stream)
|
||||
{
|
||||
// If we don't have a valid dictionary with values, we can't write out
|
||||
if (_keyValuePairs == null || _keyValuePairs.Count == 0)
|
||||
return false;
|
||||
|
||||
// If the stream is invalid or unwritable, we can't output to it
|
||||
if (stream == null || !stream.CanWrite || stream.Position >= stream.Length - 1)
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
using (StreamWriter sw = new StreamWriter(stream))
|
||||
{
|
||||
// Order the dictionary by keys to link sections together
|
||||
var orderedKeyValuePairs = _keyValuePairs.OrderBy(kvp => kvp.Key);
|
||||
|
||||
string section = string.Empty;
|
||||
foreach (var keyValuePair in orderedKeyValuePairs)
|
||||
{
|
||||
// Extract the key and value
|
||||
string key = keyValuePair.Key;
|
||||
string value = keyValuePair.Value;
|
||||
|
||||
// We assume '.' is a section name separator
|
||||
if (key.Contains('.'))
|
||||
{
|
||||
// Split the key by '.'
|
||||
string[] data = keyValuePair.Key.Split('.');
|
||||
|
||||
// If the key contains an '.', we need to put them back in
|
||||
string newSection = data[0].Trim();
|
||||
key = string.Join(".", data.Skip(1)).Trim();
|
||||
|
||||
// If we have a new section, write it out
|
||||
if (!string.Equals(newSection, section, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
sw.WriteLine($"[{newSection}]");
|
||||
section = newSection;
|
||||
}
|
||||
}
|
||||
|
||||
// Now write out the key and value in a standardized way
|
||||
sw.WriteLine($"{key}={value}");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// We don't care what the error was, just catch and return
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#region IDictionary Impelementations
|
||||
|
||||
public ICollection<string> Keys => ((IDictionary<string, string>)_keyValuePairs).Keys;
|
||||
|
||||
public ICollection<string> Values => ((IDictionary<string, string>)_keyValuePairs).Values;
|
||||
|
||||
public int Count => ((ICollection<KeyValuePair<string, string>>)_keyValuePairs).Count;
|
||||
|
||||
public bool IsReadOnly => ((ICollection<KeyValuePair<string, string>>)_keyValuePairs).IsReadOnly;
|
||||
|
||||
public void Add(string key, string value)
|
||||
{
|
||||
((IDictionary<string, string>)_keyValuePairs).Add(key.ToLowerInvariant(), value);
|
||||
}
|
||||
|
||||
bool IDictionary<string, string>.Remove(string key)
|
||||
{
|
||||
return ((IDictionary<string, string>)_keyValuePairs).Remove(key.ToLowerInvariant());
|
||||
}
|
||||
|
||||
public bool TryGetValue(string key, out string value)
|
||||
{
|
||||
return ((IDictionary<string, string>)_keyValuePairs).TryGetValue(key.ToLowerInvariant(), out value);
|
||||
}
|
||||
|
||||
public void Add(KeyValuePair<string, string> item)
|
||||
{
|
||||
var newItem = new KeyValuePair<string, string>(item.Key.ToLowerInvariant(), item.Value);
|
||||
((ICollection<KeyValuePair<string, string>>)_keyValuePairs).Add(newItem);
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
((ICollection<KeyValuePair<string, string>>)_keyValuePairs).Clear();
|
||||
}
|
||||
|
||||
public bool Contains(KeyValuePair<string, string> item)
|
||||
{
|
||||
var newItem = new KeyValuePair<string, string>(item.Key.ToLowerInvariant(), item.Value);
|
||||
return ((ICollection<KeyValuePair<string, string>>)_keyValuePairs).Contains(newItem);
|
||||
}
|
||||
|
||||
public bool ContainsKey(string key)
|
||||
{
|
||||
return _keyValuePairs.ContainsKey(key.ToLowerInvariant());
|
||||
}
|
||||
|
||||
public void CopyTo(KeyValuePair<string, string>[] array, int arrayIndex)
|
||||
{
|
||||
((ICollection<KeyValuePair<string, string>>)_keyValuePairs).CopyTo(array, arrayIndex);
|
||||
}
|
||||
|
||||
public bool Remove(KeyValuePair<string, string> item)
|
||||
{
|
||||
var newItem = new KeyValuePair<string, string>(item.Key.ToLowerInvariant(), item.Value);
|
||||
return ((ICollection<KeyValuePair<string, string>>)_keyValuePairs).Remove(newItem);
|
||||
}
|
||||
|
||||
public IEnumerator<KeyValuePair<string, string>> GetEnumerator()
|
||||
{
|
||||
return ((IEnumerable<KeyValuePair<string, string>>)_keyValuePairs).GetEnumerator();
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return ((IEnumerable)_keyValuePairs).GetEnumerator();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Reflection;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
@@ -15,6 +14,7 @@ using SabreTools.Library.Data;
|
||||
using SabreTools.Library.DatFiles;
|
||||
using SabreTools.Library.DatItems;
|
||||
using SabreTools.Library.FileTypes;
|
||||
using SabreTools.Library.Readers;
|
||||
using SabreTools.Library.Reports;
|
||||
using SabreTools.Library.Skippers;
|
||||
using Compress.ThreadReaders;
|
||||
@@ -1737,6 +1737,28 @@ namespace SabreTools.Library.Tools
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the IniReader associated with a file, if possible
|
||||
/// </summary>
|
||||
/// <param name="filename">Name of the file to be parsed</param>
|
||||
/// <param name="validateRows">True if rows should be in a proper format, false if invalid is okay</param>
|
||||
/// <returns>The IniReader representing the (possibly converted) file, null otherwise</returns>
|
||||
public static IniReader GetIniReader(string filename, bool validateRows)
|
||||
{
|
||||
Globals.Logger.Verbose($"Attempting to read file: {filename}");
|
||||
|
||||
// Check if file exists
|
||||
if (!File.Exists(filename))
|
||||
{
|
||||
Globals.Logger.Warning($"File '{filename}' could not read from!");
|
||||
return null;
|
||||
}
|
||||
|
||||
IniReader ir = new IniReader(filename);
|
||||
ir.ValidateRows = validateRows;
|
||||
return ir;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve a list of just files from inputs
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user