Skip to content

Commit

Permalink
Fix MSG parse issue with CPTDTXT.MSG (#557)
Browse files Browse the repository at this point in the history
* Perform flushing of large line buffers if necessary to prevent overflows
in LineBreaker

* Fix MSG parse issue with CPTDTXT.MSG

MSG parsing code would enter a junk state when encountering a \r\n
immediately after an identifier, but it should allow the transition
to the SPACE state instead. Technically \r\n are space characters
after all.

* Msg Parser Refactor

* Handle Help Text Before MSG Key

* Ignore CR in MSG File Values

* First Swag at Update Values

* Unit Tests passing locally

* Added XML Comments on Methods

* Clarification

* Unit Tests for MsgFile Parser Methods

* Use Char Values (Easier to Read)

* Specific Unit Test for Weird MajorMUD Formatting

* Removed Debug

* Update MSG File Integration Tests for Escape Characters

* Fix Issue of too many characters being truncated when escaping a curly bracket

* Fix Issue of no space between KEY and VALUE curly Bracket

- Properly parse if format is `KEY{VALUE}`
- Unit + Integration Test Updates

* Rename KEY to IDENTIFIER

Co-authored-by: Eric P. Nusbaum <eric@enusbaum.com>
  • Loading branch information
paladine and enusbaum committed Apr 28, 2022
1 parent cd0125f commit 15ff7cf
Show file tree
Hide file tree
Showing 5 changed files with 570 additions and 189 deletions.
21 changes: 21 additions & 0 deletions MBBSEmu.Tests/Assets/IntegrationTest.msg
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
LEVEL0 {MBBSEmu Test MSG File for MajorMUD Scenarios}

LEVEL1 {Hardware Setup Options}

LEVEL3 {Security and Accounting Options}

LEVEL4 {Configuration Options}

TEST1{This is topic 1: value} S 30 Help Topic String

LEVEL6 {Text Editable Blocks}

TEST2 {This is topic 2: value} S 30 Help Topic 2

String

TEST3 {This is topic 3: value} S 40 Help Topic 3 String

TEST4 {Escaped ~~ Values ~} Test}

LEVEL8 {FSE Help Messages}
2 changes: 2 additions & 0 deletions MBBSEmu.Tests/MBBSEmu.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
</PropertyGroup>

<ItemGroup>
<None Remove="Assets\IntegrationTest.msg" />
<None Remove="Assets\Module_Multiple_NoPatch.json" />
<None Remove="Assets\Module_Multiple_Patch.json" />
<None Remove="Assets\Module_Single_NoPatch.json" />
Expand Down Expand Up @@ -46,6 +47,7 @@
<EmbeddedResource Include="Assets\Module_Multiple_Patch.json" />
<EmbeddedResource Include="Assets\Module_Single_NoPatch.json" />
<EmbeddedResource Include="Assets\Module_Single_Patch.json" />
<EmbeddedResource Include="Assets\IntegrationTest.msg" />
</ItemGroup>

<ItemGroup>
Expand Down
272 changes: 194 additions & 78 deletions MBBSEmu.Tests/Module/MsgFile_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,102 +10,218 @@

namespace MBBSEmu.Tests.Module
{
public class MsgFile_Tests : TestBase, IDisposable
{
private readonly string _modulePath;

private MemoryStream Load(string resourceFile)
public class MsgFile_Tests : TestBase, IDisposable
{
var resource = ResourceManager.GetTestResourceManager().GetResource($"MBBSEmu.Tests.Assets.{resourceFile}");
return new MemoryStream(resource.ToArray());
}
private readonly string _modulePath;

public MsgFile_Tests()
{
_modulePath = GetModulePath();
}
private MemoryStream Load(string resourceFile)
{
var resource = ResourceManager.GetTestResourceManager().GetResource($"MBBSEmu.Tests.Assets.{resourceFile}");
return new MemoryStream(resource.ToArray());
}

public void Dispose()
{
if (Directory.Exists(_modulePath))
public MsgFile_Tests()
{
Directory.Delete(_modulePath, recursive: true);
_modulePath = GetModulePath();
}
}

[Fact]
public void ReplaceWithEmptyDictionary()
{
var sourceMessage = Load("MBBSEMU.MSG");
var outputRawStream = new MemoryStream();
using var sourceStream = new StreamStream(sourceMessage);
using var outputStream = new StreamStream(outputRawStream);
public void Dispose()
{
if (Directory.Exists(_modulePath))
{
Directory.Delete(_modulePath, recursive: true);
}
}

MsgFile.UpdateValues(sourceStream, outputStream, new Dictionary<string, string>());
[Fact]
public void ReplaceWithEmptyDictionary()
{
var sourceMessage = Load("MBBSEMU.MSG");
var outputRawStream = new MemoryStream();
using var sourceStream = new StreamStream(sourceMessage);
using var outputStream = new StreamStream(outputRawStream);

outputRawStream.Flush();
outputRawStream.Seek(0, SeekOrigin.Begin);
var result = outputRawStream.ToArray();
MsgFile.UpdateValues(sourceStream, outputStream, new Dictionary<string, string>());

sourceMessage.Seek(0, SeekOrigin.Begin);
var expected = sourceMessage.ToArray();
outputRawStream.Flush();
outputRawStream.Seek(0, SeekOrigin.Begin);
var result = outputRawStream.ToArray();

result.Should().BeEquivalentTo(expected);
}

[Fact]
public void ReplaceWithActualValues()
{
var sourceMessage = Load("MBBSEMU.MSG");
var outputRawStream = new MemoryStream();
using var sourceStream = new StreamStream(sourceMessage);
using var outputStream = new StreamStream(outputRawStream);

MsgFile.UpdateValues(sourceStream, outputStream, new Dictionary<string, string>() {{"SOCCCR", "128"}, {"SLOWTICS", "Whatever"}, {"MAXITEM", "45"}});

outputRawStream.Flush();
outputRawStream.Seek(0, SeekOrigin.Begin);
var result = Encoding.ASCII.GetString(outputRawStream.ToArray());

// expected should have the mods applied
var expected = Encoding.ASCII.GetString(Load("MBBSEMU.MSG").ToArray());
expected = expected.Replace("SOCCCR {SoC credit consumption rate adjustment, per min: 0}", "SOCCCR {SoC credit consumption rate adjustment, per min: 128}");
expected = expected.Replace("SLOWTICS {Slow system factor: 10000}", "SLOWTICS {Slow system factor: Whatever}");
expected = expected.Replace("MAXITEM {Maximum number of items: 954}", "MAXITEM {Maximum number of items: 45}");

result.Should().Be(expected);
}
sourceMessage.Seek(0, SeekOrigin.Begin);
var expected = sourceMessage.ToArray();

[Fact]
public void ReplaceFileEmptyDictionary()
{
var fileName = Path.Combine(_modulePath, "MBBSEMU.MSG");
result.Should().BeEquivalentTo(expected);
}

Directory.CreateDirectory(_modulePath);
File.WriteAllBytes(fileName, Load("MBBSEMU.MSG").ToArray());
[Fact]
public void ReplaceWithActualValues()
{
var sourceMessage = Load("MBBSEMU.MSG");
var outputRawStream = new MemoryStream();
using var sourceStream = new StreamStream(sourceMessage);
using var outputStream = new StreamStream(outputRawStream);

MsgFile.UpdateValues(fileName, new Dictionary<string, string>());
MsgFile.UpdateValues(sourceStream, outputStream, new Dictionary<string, string>() { { "SOCCCR", "128" }, { "SLOWTICS", "Whatever" }, { "MAXITEM", "45" } });

File.ReadAllBytes(fileName).Should().BeEquivalentTo(Load("MBBSEMU.MSG").ToArray());
}
outputRawStream.Flush();
outputRawStream.Seek(0, SeekOrigin.Begin);
var result = Encoding.ASCII.GetString(outputRawStream.ToArray());

[Fact]
public void ReplaceFileWithActualValues()
{
var fileName = Path.Combine(_modulePath, "MBBSEMU.MSG");
// expected should have the mods applied
var expected = Encoding.ASCII.GetString(Load("MBBSEMU.MSG").ToArray());
expected = expected.Replace("SOCCCR {SoC credit consumption rate adjustment, per min: 0}", "SOCCCR {SoC credit consumption rate adjustment, per min: 128}");
expected = expected.Replace("SLOWTICS {Slow system factor: 10000}", "SLOWTICS {Slow system factor: Whatever}");
expected = expected.Replace("MAXITEM {Maximum number of items: 954}", "MAXITEM {Maximum number of items: 45}");

result.Should().Be(expected);
}

[Fact]
public void ReplaceFileEmptyDictionary()
{
var fileName = Path.Combine(_modulePath, "MBBSEMU.MSG");

Directory.CreateDirectory(_modulePath);
File.WriteAllBytes(fileName, Load("MBBSEMU.MSG").ToArray());

MsgFile.UpdateValues(fileName, new Dictionary<string, string>());

File.ReadAllBytes(fileName).Should().BeEquivalentTo(Load("MBBSEMU.MSG").ToArray());
}

[Fact]
public void ReplaceFileWithActualValues()
{
var fileName = Path.Combine(_modulePath, "MBBSEMU.MSG");

Directory.CreateDirectory(_modulePath);
File.WriteAllBytes(fileName, Load("MBBSEMU.MSG").ToArray());

MsgFile.UpdateValues(fileName, new Dictionary<string, string>() { { "SOCCCR", "128" }, { "SLOWTICS", "Whatever" }, { "MAXITEM", "45" } });

Directory.CreateDirectory(_modulePath);
File.WriteAllBytes(fileName, Load("MBBSEMU.MSG").ToArray());
// expected should have the mods applied
var expected = Encoding.ASCII.GetString(Load("MBBSEMU.MSG").ToArray());
expected = expected.Replace("SOCCCR {SoC credit consumption rate adjustment, per min: 0}", "SOCCCR {SoC credit consumption rate adjustment, per min: 128}");
expected = expected.Replace("SLOWTICS {Slow system factor: 10000}", "SLOWTICS {Slow system factor: Whatever}");
expected = expected.Replace("MAXITEM {Maximum number of items: 954}", "MAXITEM {Maximum number of items: 45}");

MsgFile.UpdateValues(fileName, new Dictionary<string, string>() {{"SOCCCR", "128"}, {"SLOWTICS", "Whatever"}, {"MAXITEM", "45"}});
File.ReadAllBytes(fileName).Should().BeEquivalentTo(Encoding.ASCII.GetBytes(expected));
}

[Theory]
[InlineData('\r', ' ')]
[InlineData('~', '~')]
public void ProcessValue_IgnoredValues(char currentCharacter, char previousCharacter)
{
var resultCharacter = MsgFile.ProcessValue(currentCharacter, previousCharacter, out var resultState);

Assert.Equal(0, resultCharacter);
Assert.Equal(MsgFile.MsgParseState.VALUE, resultState);
}

[Fact]
public void ProcessValue_EscapedBracket()
{
var resultCharacter = MsgFile.ProcessValue('}', '~', out var resultState);

Assert.Equal(0, resultCharacter);
Assert.Equal(MsgFile.MsgParseState.ESCAPEBRACKET, resultState);
}

[Fact]
public void ProcessValue_ClosingBracket()
{
var resultCharacter = MsgFile.ProcessValue('}', ' ', out var resultState);

Assert.Equal(0, resultCharacter);
Assert.Equal(MsgFile.MsgParseState.POSTVALUE, resultState);
}

[Theory]
[InlineData('A', MsgFile.MsgParseState.IDENTIFIER)]
[InlineData('Z', MsgFile.MsgParseState.IDENTIFIER)]
[InlineData('1', MsgFile.MsgParseState.IDENTIFIER)]
[InlineData('0', MsgFile.MsgParseState.IDENTIFIER)]
[InlineData(' ', MsgFile.MsgParseState.PREKEY)]
[InlineData('\r', MsgFile.MsgParseState.PREKEY)]
[InlineData('\n', MsgFile.MsgParseState.PREKEY)]
[InlineData('!', MsgFile.MsgParseState.PREKEY)]
[InlineData('{', MsgFile.MsgParseState.PREKEY)]
[InlineData('}', MsgFile.MsgParseState.PREKEY)]
public void ProcessPreKey_Tests(char currentCharacter, MsgFile.MsgParseState expectedState)
{
var resultCharacter = MsgFile.ProcessPreKey(currentCharacter, out var resultState);

Assert.Equal(currentCharacter, resultCharacter);
Assert.Equal(expectedState, resultState);
}

[Theory]
[InlineData('A', MsgFile.MsgParseState.IDENTIFIER)]
[InlineData('Z', MsgFile.MsgParseState.IDENTIFIER)]
[InlineData('1', MsgFile.MsgParseState.IDENTIFIER)]
[InlineData('0', MsgFile.MsgParseState.IDENTIFIER)]
[InlineData(' ', MsgFile.MsgParseState.POSTKEY)]
[InlineData('\r', MsgFile.MsgParseState.POSTKEY)]
[InlineData('\n', MsgFile.MsgParseState.POSTKEY)]
[InlineData('!', MsgFile.MsgParseState.POSTKEY)]
[InlineData('{', MsgFile.MsgParseState.VALUE)]
[InlineData('}', MsgFile.MsgParseState.POSTKEY)]
public void ProcessKey_Tests(char currentCharacter, MsgFile.MsgParseState expectedState)
{
var resultCharacter = MsgFile.ProcessKey(currentCharacter, out var resultState);

Assert.Equal(currentCharacter, resultCharacter);
Assert.Equal(expectedState, resultState);
}

[Theory]
[InlineData('{', MsgFile.MsgParseState.VALUE)]
[InlineData('\r', MsgFile.MsgParseState.POSTKEY)] //MajorMUD puts the key on its own line
[InlineData('\n', MsgFile.MsgParseState.POSTKEY)] //MajorMUD puts the key on its own line
[InlineData('A', MsgFile.MsgParseState.IDENTIFIER)]
[InlineData('Z', MsgFile.MsgParseState.IDENTIFIER)]
[InlineData('1', MsgFile.MsgParseState.IDENTIFIER)]
[InlineData('0', MsgFile.MsgParseState.IDENTIFIER)]
[InlineData(' ', MsgFile.MsgParseState.POSTKEY)]
public void ProcessPostKey_Tests(char currentCharacter, MsgFile.MsgParseState expectedState)
{
var resultCharacter = MsgFile.ProcessPostKey(currentCharacter, out var resultState);

Assert.Equal(currentCharacter, resultCharacter);
Assert.Equal(expectedState, resultState);
}

[Theory]
[InlineData('\n', MsgFile.MsgParseState.PREKEY)]
[InlineData('A', MsgFile.MsgParseState.POSTVALUE)]
[InlineData('Z', MsgFile.MsgParseState.POSTVALUE)]
[InlineData('1', MsgFile.MsgParseState.POSTVALUE)]
[InlineData('0', MsgFile.MsgParseState.POSTVALUE)]
[InlineData(' ', MsgFile.MsgParseState.POSTVALUE)]
[InlineData('\r', MsgFile.MsgParseState.POSTVALUE)]
[InlineData('!', MsgFile.MsgParseState.POSTVALUE)]
[InlineData('{', MsgFile.MsgParseState.POSTVALUE)]
[InlineData('}', MsgFile.MsgParseState.POSTVALUE)]
public void ProcessPostValue_Tests(char currentCharacter, MsgFile.MsgParseState expectedState)
{
var resultCharacter = MsgFile.ProcessPostValue(currentCharacter, out var resultState);

Assert.Equal(currentCharacter, resultCharacter);
Assert.Equal(expectedState, resultState);
}

[Fact]
public void LoadMsg_IntegrationTest()
{
var sourceMessage = Load("IntegrationTest.msg");

// expected should have the mods applied
var expected = Encoding.ASCII.GetString(Load("MBBSEMU.MSG").ToArray());
expected = expected.Replace("SOCCCR {SoC credit consumption rate adjustment, per min: 0}", "SOCCCR {SoC credit consumption rate adjustment, per min: 128}");
expected = expected.Replace("SLOWTICS {Slow system factor: 10000}", "SLOWTICS {Slow system factor: Whatever}");
expected = expected.Replace("MAXITEM {Maximum number of items: 954}", "MAXITEM {Maximum number of items: 45}");
var msgValues = MsgFile.ExtractMsgValues(sourceMessage.ToArray());

File.ReadAllBytes(fileName).Should().BeEquivalentTo(Encoding.ASCII.GetBytes(expected));
Assert.Equal("This is topic 1: value\0", Encoding.ASCII.GetString(msgValues[4]));
Assert.Equal("This is topic 2: value\0", Encoding.ASCII.GetString(msgValues[6]));
Assert.Equal("This is topic 3: value\0", Encoding.ASCII.GetString(msgValues[7]));
Assert.Equal("Escaped ~ Values } Test\0", Encoding.ASCII.GetString(msgValues[8]));
}
}
}
}
2 changes: 1 addition & 1 deletion MBBSEmu/Module/McvFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ private ReadOnlySpan<byte> GetMessageValue(int ordinal)

for (var i = 1; i <= message.Length; i++)
{
if (message[^i] == 0x3A || message[^i] == 0x20)
if (message[^i] == ':' || message[^i] == ' ')
return message.Slice(message.Length - i, i);
}

Expand Down
Loading

0 comments on commit 15ff7cf

Please sign in to comment.