Skip to content

Commit

Permalink
Update INI file and add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
mnadareski committed Nov 26, 2024
1 parent ec8908a commit 450d8aa
Show file tree
Hide file tree
Showing 3 changed files with 132 additions and 75 deletions.
67 changes: 67 additions & 0 deletions SabreTools.IO.Test/IniFileTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
using System.IO;
using System.Text;
using Xunit;

namespace SabreTools.IO.Test
{
public class IniFileTests
{
[Fact]
public void EndToEndTest()
{
string expected = "[section1]\nkey1=value1\nkey2=value2\n";

// Build the INI
var iniFile = new IniFile();
iniFile.AddOrUpdate("section1.key1", "value1");
iniFile["section1.key2"] = "value2";
iniFile["section2.key3"] = "REMOVEME";
bool removed = iniFile.Remove("section2.key3");

Assert.True(removed);
Assert.Equal("value1", iniFile["section1.key1"]);
Assert.Equal("value2", iniFile["section1.key2"]);

// Write the INI
var stream = new MemoryStream();
bool write = iniFile.Write(stream);

// Length includes UTF-8 BOM
Assert.True(write);
Assert.Equal(38, stream.Length);
string actual = Encoding.UTF8.GetString(stream.ToArray(), 3, (int)stream.Length - 3);
Assert.Equal(expected, actual);

// Parse the INI
stream.Seek(0, SeekOrigin.Begin);
var secondIni = new IniFile(stream);
Assert.Equal("value1", secondIni["section1.key1"]);
Assert.Equal("value2", secondIni["section1.key2"]);
}

[Fact]
public void RemoveInvalidKeyTest()
{
var iniFile = new IniFile();
bool removed = iniFile.Remove("invalid.key");
Assert.False(removed);
}

[Fact]
public void ReadEmptyStreamTest()
{
var stream = new MemoryStream();
var iniFile = new IniFile(stream);
Assert.Empty(iniFile);
}

[Fact]
public void WriteEmptyIniFileTest()
{
var iniFile = new IniFile();
var stream = new MemoryStream();
bool write = iniFile.Write(stream);
Assert.False(write);
}
}
}
138 changes: 64 additions & 74 deletions SabreTools.IO/IniFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,16 +44,19 @@ public IniFile()
/// </summary>
public IniFile(string path)
{
Parse(path);
// If we don't have a file, we can't read it
if (!File.Exists(path))
throw new FileNotFoundException(nameof(path));

using var fileStream = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
Parse(fileStream);
}

/// <summary>
/// Populate an INI file from stream
/// </summary>
public IniFile(Stream stream)
{
Parse(stream);
}
=> Parse(stream);

/// <summary>
/// Add or update a key and value to the INI file
Expand All @@ -77,74 +80,6 @@ public bool Remove(string key)
return false;
}

/// <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.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
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
{
// TODO: Can we use the section header in the reader?
using var reader = new IniReader(stream, Encoding.UTF8);

string? section = string.Empty;
while (!reader.EndOfStream)
{
// If we dont have a next line
if (!reader.ReadNextLine())
break;

// Process the row according to type
switch (reader.RowType)
{
case IniRowType.SectionHeader:
section = reader.Section;
break;

case IniRowType.KeyValue:
string? key = reader.KeyValuePair?.Key;

// 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
this[key] = reader.KeyValuePair?.Value;
break;

default:
// No-op
break;
}
}
}
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>
Expand All @@ -167,8 +102,8 @@ public bool Write(Stream stream)
if (_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)
// If the stream is invalid, we can't output to it
if (!stream.CanWrite)
return false;

try
Expand Down Expand Up @@ -220,6 +155,61 @@ public bool Write(Stream stream)
return true;
}

/// <summary>
/// Read an INI file from a stream
/// </summary>
private 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
{
// TODO: Can we use the section header in the reader?
using var reader = new IniReader(stream, Encoding.UTF8);

string? section = string.Empty;
while (!reader.EndOfStream)
{
// If we dont have a next line
if (!reader.ReadNextLine())
break;

// Process the row according to type
switch (reader.RowType)
{
case IniRowType.SectionHeader:
section = reader.Section;
break;

case IniRowType.KeyValue:
string? key = reader.KeyValuePair?.Key;

// 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
this[key] = reader.KeyValuePair?.Value;
break;

default:
// No-op
break;
}
}
}
catch
{
// We don't care what the error was, just catch and return
return false;
}

return true;
}

#region IDictionary Impelementations

public ICollection<string> Keys => _keyValuePairs.Keys;
Expand Down
2 changes: 1 addition & 1 deletion SabreTools.IO/Writers/IniWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public IniWriter(string filename)
/// </summary>
public IniWriter(Stream stream, Encoding encoding)
{
sw = new StreamWriter(stream, encoding);
sw = new StreamWriter(stream, encoding, 1024, leaveOpen: true);

Check failure on line 27 in SabreTools.IO/Writers/IniWriter.cs

View workflow job for this annotation

GitHub Actions / build

The best overload for 'StreamWriter' does not have a parameter named 'leaveOpen'

Check failure on line 27 in SabreTools.IO/Writers/IniWriter.cs

View workflow job for this annotation

GitHub Actions / build

The best overload for 'StreamWriter' does not have a parameter named 'leaveOpen'

Check failure on line 27 in SabreTools.IO/Writers/IniWriter.cs

View workflow job for this annotation

GitHub Actions / build

The best overload for 'StreamWriter' does not have a parameter named 'leaveOpen'

Check failure on line 27 in SabreTools.IO/Writers/IniWriter.cs

View workflow job for this annotation

GitHub Actions / build

The best overload for 'StreamWriter' does not have a parameter named 'leaveOpen'

Check failure on line 27 in SabreTools.IO/Writers/IniWriter.cs

View workflow job for this annotation

GitHub Actions / build

The best overload for 'StreamWriter' does not have a parameter named 'leaveOpen'

Check failure on line 27 in SabreTools.IO/Writers/IniWriter.cs

View workflow job for this annotation

GitHub Actions / build

The best overload for 'StreamWriter' does not have a parameter named 'leaveOpen'
}

/// <summary>
Expand Down

0 comments on commit 450d8aa

Please sign in to comment.