Skip to content

Commit

Permalink
optimize parser
Browse files Browse the repository at this point in the history
  • Loading branch information
tinohager committed Dec 9, 2024
1 parent 73e1567 commit cc21fdc
Show file tree
Hide file tree
Showing 4 changed files with 79 additions and 11 deletions.
16 changes: 8 additions & 8 deletions src/Nager.EmailAuthentication.UnitTest/DmarcRecordParserTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ public sealed class DmarcRecordParserTest
[TestMethod]
public void TryParse_InvalidDmarcString1_ReturnsTrueAndPopulatesDmarcRecord()
{
var isSuccessful = DmarcRecordParser.TryParse("v=DMARC", out var dmarcDataFragment, out var unrecognizedParts);
var isSuccessful = DmarcRecordParser.TryParse("v=DMARC", out var dmarcDataFragment, out var unrecognizedParts, out var parseErrors);
Assert.IsTrue(isSuccessful);
Assert.IsNotNull(dmarcDataFragment);
Assert.IsNull(unrecognizedParts);
Expand All @@ -15,7 +15,7 @@ public void TryParse_InvalidDmarcString1_ReturnsTrueAndPopulatesDmarcRecord()
[TestMethod]
public void TryParse_InvalidDmarcString2_ReturnsTrueAndPopulatesDmarcRecord()
{
var isSuccessful = DmarcRecordParser.TryParse("v=DMARC1", out var dmarcDataFragment, out var unrecognizedParts);
var isSuccessful = DmarcRecordParser.TryParse("v=DMARC1", out var dmarcDataFragment, out var unrecognizedParts, out var parseErrors);
Assert.IsTrue(isSuccessful);
Assert.IsNotNull(dmarcDataFragment);
Assert.IsNull(unrecognizedParts);
Expand All @@ -24,7 +24,7 @@ public void TryParse_InvalidDmarcString2_ReturnsTrueAndPopulatesDmarcRecord()
[TestMethod]
public void TryParse_InvalidDmarcString3_ReturnsTrueAndPopulatesDmarcRecord()
{
var isSuccessful = DmarcRecordParser.TryParse("v=DMARC1; p=Test", out var dmarcDataFragment, out var unrecognizedParts);
var isSuccessful = DmarcRecordParser.TryParse("v=DMARC1; p=Test", out var dmarcDataFragment, out var unrecognizedParts, out var parseErrors);
Assert.IsTrue(isSuccessful);
Assert.IsNotNull(dmarcDataFragment);
Assert.AreEqual("Test", dmarcDataFragment.DomainPolicy);
Expand All @@ -34,7 +34,7 @@ public void TryParse_InvalidDmarcString3_ReturnsTrueAndPopulatesDmarcRecord()
[TestMethod]
public void TryParse_InvalidDmarcString4_ReturnsTrueAndPopulatesDmarcRecord()
{
var isSuccessful = DmarcRecordParser.TryParse("v=DMARC1; p=Test;", out var dmarcDataFragment, out var unrecognizedParts);
var isSuccessful = DmarcRecordParser.TryParse("v=DMARC1; p=Test;", out var dmarcDataFragment, out var unrecognizedParts, out var parseErrors);
Assert.IsTrue(isSuccessful);
Assert.IsNotNull(dmarcDataFragment);
Assert.AreEqual("Test", dmarcDataFragment.DomainPolicy);
Expand All @@ -44,7 +44,7 @@ public void TryParse_InvalidDmarcString4_ReturnsTrueAndPopulatesDmarcRecord()
[TestMethod]
public void TryParse_ValidDmarcString1_ReturnsTrueAndPopulatesDmarcRecord()
{
var isSuccessful = DmarcRecordParser.TryParse("v=DMARC1; p=reject;", out var dmarcDataFragment, out var unrecognizedParts);
var isSuccessful = DmarcRecordParser.TryParse("v=DMARC1; p=reject;", out var dmarcDataFragment, out var unrecognizedParts, out var parseErrors);
Assert.IsTrue(isSuccessful);
Assert.IsNotNull(dmarcDataFragment);
Assert.AreEqual("reject", dmarcDataFragment.DomainPolicy);
Expand All @@ -54,7 +54,7 @@ public void TryParse_ValidDmarcString1_ReturnsTrueAndPopulatesDmarcRecord()
[TestMethod]
public void TryParse_ValidDmarcString2_ReturnsTrueAndPopulatesDmarcRecord()
{
var isSuccessful = DmarcRecordParser.TryParse("v=DMARC1; p=reject; sp=none;", out var dmarcDataFragment, out var unrecognizedParts);
var isSuccessful = DmarcRecordParser.TryParse("v=DMARC1; p=reject; sp=none;", out var dmarcDataFragment, out var unrecognizedParts, out var parseErrors);
Assert.IsTrue(isSuccessful);
Assert.IsNotNull(dmarcDataFragment);
Assert.AreEqual("reject", dmarcDataFragment.DomainPolicy);
Expand All @@ -65,7 +65,7 @@ public void TryParse_ValidDmarcString2_ReturnsTrueAndPopulatesDmarcRecord()
[TestMethod]
public void TryParse_CorruptDmarcString1_ReturnsTrueAndPopulatesDmarcRecord()
{
var isSuccessful = DmarcRecordParser.TryParse("verification=123456789", out var dmarcDataFragment, out var unrecognizedParts);
var isSuccessful = DmarcRecordParser.TryParse("verification=123456789", out var dmarcDataFragment, out var unrecognizedParts, out var parseErrors);
Assert.IsTrue(isSuccessful);
Assert.IsNotNull(dmarcDataFragment);
Assert.IsNotNull(unrecognizedParts);
Expand All @@ -75,7 +75,7 @@ public void TryParse_CorruptDmarcString1_ReturnsTrueAndPopulatesDmarcRecord()
[TestMethod]
public void TryParse_CorruptDmarcString2_ReturnsTrueAndPopulatesDmarcRecord()
{
var isSuccessful = DmarcRecordParser.TryParse(" ", out var dmarcDataFragment, out var unrecognizedParts);
var isSuccessful = DmarcRecordParser.TryParse(" ", out var dmarcDataFragment, out var unrecognizedParts, out var parseErrors);
Assert.IsFalse(isSuccessful);
Assert.IsNull(dmarcDataFragment);
Assert.IsNull(unrecognizedParts);
Expand Down
33 changes: 30 additions & 3 deletions src/Nager.EmailAuthentication/DmarcRecordParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public static bool TryParse(
string dmarcRaw,
out DmarcDataFragment? dmarcDataFragment)
{
return TryParse(dmarcRaw, out dmarcDataFragment, out _);
return TryParse(dmarcRaw, out dmarcDataFragment, out _, out _);
}

/// <summary>
Expand All @@ -26,20 +26,34 @@ public static bool TryParse(
/// <param name="dmarcRaw">The raw DMARC string to parse.</param>
/// <param name="dmarcDataFragment">The parsed DMARC record, if successful.</param>
/// <param name="unrecognizedParts">A list of unrecognized parts in the DMARC string, if any.</param>
/// <param name="parseErrors">A list of errors in the DMARC string, if any.</param>
/// <returns><see langword="true"/> if parsing is successful; otherwise <see langword="false"/>.</returns>
public static bool TryParse(
string dmarcRaw,
out DmarcDataFragment? dmarcDataFragment,
out string[]? unrecognizedParts)
out string[]? unrecognizedParts,
out ParseError[]? parseErrors)
{
unrecognizedParts = null;
parseErrors = null;

var errors = new List<ParseError>();

if (string.IsNullOrWhiteSpace(dmarcRaw))
{
dmarcDataFragment = null;
return false;
}

if (dmarcRaw.StartsWith("v=DMARC1", StringComparison.OrdinalIgnoreCase))
{
errors.Add(new ParseError
{
ErrorMessage = "DMARC record is invalid: it must start with 'v=DMARC1'.",
Severity = ErrorSeverity.Critical
});
}

var keyValueParser = new KeyValueParser.MemoryEfficientKeyValueParser(';', '=');
if (!keyValueParser.TryParse(dmarcRaw, out var parseResult))
{
Expand All @@ -53,6 +67,19 @@ public static bool TryParse(
return false;
}

var duplicateConfigurations = parseResult.KeyValues
.GroupBy(o => o.Key)
.Where(g => g.Count() > 1);

foreach (var duplicate in duplicateConfigurations)
{
errors.Add(new ParseError
{
ErrorMessage = $"Duplicate configuration detected for key: '{duplicate.Key}'.",
Severity = ErrorSeverity.Warning
});
}

var dataFragment = new DmarcDataFragment();

var unrecognizedHandlers = new List<string>();
Expand Down Expand Up @@ -94,7 +121,7 @@ public static bool TryParse(
}

unrecognizedParts = unrecognizedHandlers.Count == 0 ? null : [.. unrecognizedHandlers];

parseErrors = errors.Count == 0 ? null : [.. errors];
dmarcDataFragment = dataFragment;

return true;
Expand Down
23 changes: 23 additions & 0 deletions src/Nager.EmailAuthentication/Models/ErrorSeverity.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
namespace Nager.EmailAuthentication.Models
{
/// <summary>
/// Error Severity
/// </summary>
public enum ErrorSeverity
{
/// <summary>
/// Minor issues or informational messages
/// </summary>
Info,

/// <summary>
/// Potential issues that don't invalidate the DMARC string
/// </summary>
Warning,

/// <summary>
/// Severe issues that invalidate the DMARC string
/// </summary>
Critical
}
}
18 changes: 18 additions & 0 deletions src/Nager.EmailAuthentication/Models/ParseError.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace Nager.EmailAuthentication.Models
{
/// <summary>
/// Parse Error
/// </summary>
public class ParseError
{
/// <summary>
/// Description of the error
/// </summary>
public required string ErrorMessage { get; set; }

/// <summary>
/// Severity of the error
/// </summary>
public ErrorSeverity Severity { get; set; }
}
}

0 comments on commit cc21fdc

Please sign in to comment.