diff --git a/Semantics.Strings.Identifiers/Iban.cs b/Semantics.Strings.Identifiers/Iban.cs index 41331579..ff44ac15 100644 --- a/Semantics.Strings.Identifiers/Iban.cs +++ b/Semantics.Strings.Identifiers/Iban.cs @@ -2,6 +2,9 @@ namespace ktsu.Semantics.Strings.Identifiers; +using System.Linq; +using System.Text; + using ktsu.Semantics.Strings; /// @@ -12,12 +15,20 @@ namespace ktsu.Semantics.Strings.Identifiers; [IsIban] public sealed record Iban : SemanticString { - /// Normalizes the input by stripping spaces and uppercasing. + /// + /// Normalizes the input by stripping all whitespace (spaces, tabs, line breaks) and uppercasing. + /// /// The raw input string. /// The canonical IBAN string. protected override string MakeCanonical(string input) { Ensure.NotNull(input); - return input.Replace(" ", string.Empty).ToUpperInvariant(); + StringBuilder canonical = new(input.Length); + foreach (char c in input.Where(static ch => !char.IsWhiteSpace(ch))) + { + canonical.Append(char.ToUpperInvariant(c)); + } + + return canonical.ToString(); } } diff --git a/Semantics.Strings.Identifiers/IsIbanAttribute.cs b/Semantics.Strings.Identifiers/IsIbanAttribute.cs index 233a0f38..e69f8978 100644 --- a/Semantics.Strings.Identifiers/IsIbanAttribute.cs +++ b/Semantics.Strings.Identifiers/IsIbanAttribute.cs @@ -21,7 +21,7 @@ public sealed class IsIbanAttribute : NativeSemanticStringValidationAttribute private sealed class IbanValidator : ValidationAdapter { - private const string Pattern = "^[A-Z]{2}[0-9]{2}[A-Z0-9]+$"; + private const string Pattern = "^[A-Z]{2}[0-9]{2}[A-Z0-9]+\\z"; protected override ValidationResult ValidateValue(string value) { diff --git a/Semantics.Strings.Identifiers/IsUlidAttribute.cs b/Semantics.Strings.Identifiers/IsUlidAttribute.cs index 0a380eb5..03e3b2a6 100644 --- a/Semantics.Strings.Identifiers/IsUlidAttribute.cs +++ b/Semantics.Strings.Identifiers/IsUlidAttribute.cs @@ -20,7 +20,7 @@ public sealed class IsUlidAttribute : NativeSemanticStringValidationAttribute private sealed class UlidValidator : ValidationAdapter { - private const string Pattern = "^[0-7][0-9A-HJKMNP-TV-Z]{25}$"; + private const string Pattern = "^[0-7][0-9A-HJKMNP-TV-Z]{25}\\z"; protected override ValidationResult ValidateValue(string value) => Regex.IsMatch(value, Pattern, RegexOptions.None, TimeSpan.FromSeconds(1)) diff --git a/Semantics.Strings.Identifiers/IsUuidAttribute.cs b/Semantics.Strings.Identifiers/IsUuidAttribute.cs index 79c19a92..c315feef 100644 --- a/Semantics.Strings.Identifiers/IsUuidAttribute.cs +++ b/Semantics.Strings.Identifiers/IsUuidAttribute.cs @@ -20,7 +20,7 @@ public sealed class IsUuidAttribute : NativeSemanticStringValidationAttribute private sealed class UuidValidator : ValidationAdapter { - private const string Pattern = "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"; + private const string Pattern = "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\\z"; protected override ValidationResult ValidateValue(string value) => Regex.IsMatch(value, Pattern, RegexOptions.None, TimeSpan.FromSeconds(1)) diff --git a/Semantics.Test/Identifiers/IbanTests.cs b/Semantics.Test/Identifiers/IbanTests.cs index 4779ec32..520016c9 100644 --- a/Semantics.Test/Identifiers/IbanTests.cs +++ b/Semantics.Test/Identifiers/IbanTests.cs @@ -44,4 +44,24 @@ public void Create_Empty_Throws() { Assert.ThrowsExactly(() => Iban.Create(string.Empty)); } + + [TestMethod] + [DataRow("GB67WEST12345698765432\n")] + [DataRow("GB67WEST12345698765432\r\n")] + public void Create_InvalidChecksumWithTrailingNewline_Throws(string input) + { + Assert.ThrowsExactly(() => Iban.Create(input)); + } + + [TestMethod] + [DataRow("GB82WEST12345698765432\n")] + [DataRow("GB82WEST12345698765432\t")] + [DataRow("\tGB82WEST12345698765432\r\n")] + [DataRow("GB82 WEST 1234 5698 7654 32\n")] + [DataRow("GB82\u00A0WEST\u00A012345698765432")] + public void Create_ValidIbanWithWhitespace_StripsItAndValidates(string input) + { + Iban iban = Iban.Create(input); + Assert.AreEqual("GB82WEST12345698765432", iban.WeakString); + } } diff --git a/Semantics.Test/Identifiers/UlidTests.cs b/Semantics.Test/Identifiers/UlidTests.cs index 3041b62b..a5c51c9a 100644 --- a/Semantics.Test/Identifiers/UlidTests.cs +++ b/Semantics.Test/Identifiers/UlidTests.cs @@ -2,6 +2,7 @@ namespace ktsu.Semantics.Test.Identifiers; +using ktsu.Semantics.Strings; using ktsu.Semantics.Strings.Identifiers; [TestClass] @@ -44,4 +45,19 @@ public void Create_Empty_Throws() { Assert.ThrowsExactly(() => Ulid.Create(string.Empty)); } + + // Ulid trims its input, which hides the attribute's own anchoring; a type that applies [IsUlid] + // without canonicalizing sees the raw value. + [IsUlid] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1812", Justification = "Used via generic type references")] + private sealed partial record UntrimmedUlid : SemanticString { } + + [TestMethod] + [DataRow("01ARZ3NDEKTSV4RRFFQ69G5FAV\n")] + [DataRow("01ARZ3NDEKTSV4RRFFQ69G5FAV\r\n")] + public void IsUlid_OnUntrimmedType_RejectsTrailingNewline(string input) + { + Assert.ThrowsExactly(() => SemanticString.Create(input)); + Assert.AreEqual("01ARZ3NDEKTSV4RRFFQ69G5FAV", SemanticString.Create("01ARZ3NDEKTSV4RRFFQ69G5FAV").WeakString); + } } diff --git a/Semantics.Test/Identifiers/UuidTests.cs b/Semantics.Test/Identifiers/UuidTests.cs index e2952aad..cc031556 100644 --- a/Semantics.Test/Identifiers/UuidTests.cs +++ b/Semantics.Test/Identifiers/UuidTests.cs @@ -2,6 +2,7 @@ namespace ktsu.Semantics.Test.Identifiers; +using ktsu.Semantics.Strings; using ktsu.Semantics.Strings.Identifiers; [TestClass] @@ -62,4 +63,19 @@ public void As_RoundTrip_PreservesValue() Assert.AreEqual(uuid, roundTripped); Assert.AreEqual(uuid.WeakString, roundTripped.WeakString); } + + // Uuid trims its input, which hides the attribute's own anchoring; a type that applies [IsUuid] + // without canonicalizing sees the raw value. + [IsUuid] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1812", Justification = "Used via generic type references")] + private sealed partial record UntrimmedUuid : SemanticString { } + + [TestMethod] + [DataRow("123e4567-e89b-12d3-a456-426614174000\n")] + [DataRow("123e4567-e89b-12d3-a456-426614174000\r\n")] + public void IsUuid_OnUntrimmedType_RejectsTrailingNewline(string input) + { + Assert.ThrowsExactly(() => SemanticString.Create(input)); + Assert.AreEqual("123e4567-e89b-12d3-a456-426614174000", SemanticString.Create("123e4567-e89b-12d3-a456-426614174000").WeakString); + } }