From cc21fdc8e257f7c2e7abe6eb977729be93675068 Mon Sep 17 00:00:00 2001 From: Tino Hager Date: Mon, 9 Dec 2024 22:18:09 +0100 Subject: [PATCH] optimize parser --- .../DmarcRecordParserTest.cs | 16 ++++----- .../DmarcRecordParser.cs | 33 +++++++++++++++++-- .../Models/ErrorSeverity.cs | 23 +++++++++++++ .../Models/ParseError.cs | 18 ++++++++++ 4 files changed, 79 insertions(+), 11 deletions(-) create mode 100644 src/Nager.EmailAuthentication/Models/ErrorSeverity.cs create mode 100644 src/Nager.EmailAuthentication/Models/ParseError.cs diff --git a/src/Nager.EmailAuthentication.UnitTest/DmarcRecordParserTest.cs b/src/Nager.EmailAuthentication.UnitTest/DmarcRecordParserTest.cs index 3cec0d4..8ba7cc5 100644 --- a/src/Nager.EmailAuthentication.UnitTest/DmarcRecordParserTest.cs +++ b/src/Nager.EmailAuthentication.UnitTest/DmarcRecordParserTest.cs @@ -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); @@ -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); @@ -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); @@ -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); @@ -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); @@ -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); @@ -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); @@ -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); diff --git a/src/Nager.EmailAuthentication/DmarcRecordParser.cs b/src/Nager.EmailAuthentication/DmarcRecordParser.cs index 81dd134..7b0608f 100644 --- a/src/Nager.EmailAuthentication/DmarcRecordParser.cs +++ b/src/Nager.EmailAuthentication/DmarcRecordParser.cs @@ -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 _); } /// @@ -26,13 +26,18 @@ public static bool TryParse( /// The raw DMARC string to parse. /// The parsed DMARC record, if successful. /// A list of unrecognized parts in the DMARC string, if any. + /// A list of errors in the DMARC string, if any. /// if parsing is successful; otherwise . 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(); if (string.IsNullOrWhiteSpace(dmarcRaw)) { @@ -40,6 +45,15 @@ public static bool TryParse( 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)) { @@ -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(); @@ -94,7 +121,7 @@ public static bool TryParse( } unrecognizedParts = unrecognizedHandlers.Count == 0 ? null : [.. unrecognizedHandlers]; - + parseErrors = errors.Count == 0 ? null : [.. errors]; dmarcDataFragment = dataFragment; return true; diff --git a/src/Nager.EmailAuthentication/Models/ErrorSeverity.cs b/src/Nager.EmailAuthentication/Models/ErrorSeverity.cs new file mode 100644 index 0000000..8c96924 --- /dev/null +++ b/src/Nager.EmailAuthentication/Models/ErrorSeverity.cs @@ -0,0 +1,23 @@ +namespace Nager.EmailAuthentication.Models +{ + /// + /// Error Severity + /// + public enum ErrorSeverity + { + /// + /// Minor issues or informational messages + /// + Info, + + /// + /// Potential issues that don't invalidate the DMARC string + /// + Warning, + + /// + /// Severe issues that invalidate the DMARC string + /// + Critical + } +} diff --git a/src/Nager.EmailAuthentication/Models/ParseError.cs b/src/Nager.EmailAuthentication/Models/ParseError.cs new file mode 100644 index 0000000..7e6821f --- /dev/null +++ b/src/Nager.EmailAuthentication/Models/ParseError.cs @@ -0,0 +1,18 @@ +namespace Nager.EmailAuthentication.Models +{ + /// + /// Parse Error + /// + public class ParseError + { + /// + /// Description of the error + /// + public required string ErrorMessage { get; set; } + + /// + /// Severity of the error + /// + public ErrorSeverity Severity { get; set; } + } +}