diff --git a/Semantics.Color/Color.Conversions.cs b/Semantics.Color/Color.Conversions.cs index c13a1e6..2bce541 100644 --- a/Semantics.Color/Color.Conversions.cs +++ b/Semantics.Color/Color.Conversions.cs @@ -45,7 +45,7 @@ public Vector3 ToSrgbVector3() /// The hex color string. /// The linear-RGB color. /// Thrown when is null. - /// Thrown when is not a recognised hex length. + /// Thrown when is not a recognised hex length, or contains a character that is not a hex digit. public static Color FromHex(string hex) { Ensure.NotNull(hex); @@ -62,6 +62,16 @@ public static Color FromHex(string hex) throw new ArgumentException("Hex color must be #RGB, #RRGGBB, or #RRGGBBAA.", nameof(hex)); } + // Convert.ToByte(s, 16) accepts a leading '+' and throws FormatException for other bad + // characters, so every character is checked here and only hex digits reach it. + foreach (char c in h) + { + if (!IsHexDigit(c)) + { + throw new ArgumentException($"Hex color contains '{c}', which is not a hex digit.", nameof(hex)); + } + } + byte r = ParseByte(h, 0); byte g = ParseByte(h, 2); byte b = ParseByte(h, 4); @@ -99,6 +109,9 @@ public static Color FromBytes(byte r, byte g, byte b, byte a = 255) => private static byte ParseByte(string hex, int index) => Convert.ToByte(hex.Substring(index, 2), 16); + private static bool IsHexDigit(char c) => + c is (>= '0' and <= '9') or (>= 'A' and <= 'F') or (>= 'a' and <= 'f'); + /// Converts this linear color to . /// The Oklab equivalent. public Oklab ToOklab() => Oklab.FromColor(this); diff --git a/Semantics.Test/Colors/HexConversionTests.cs b/Semantics.Test/Colors/HexConversionTests.cs index e92d49c..e13682e 100644 --- a/Semantics.Test/Colors/HexConversionTests.cs +++ b/Semantics.Test/Colors/HexConversionTests.cs @@ -58,4 +58,27 @@ public void ToHex_EmitsAlphaWhenNotOpaque() [TestMethod] public void FromHex_InvalidLength_Throws() => Assert.ThrowsExactly(() => Color.FromHex("#12345")); + + [TestMethod] + [DataRow("#+F+F+F")] + [DataRow("+F+F+F")] + [DataRow("#00+F00")] + [DataRow("#GGGGGG")] + [DataRow("#0x0x0x")] + [DataRow("#-1-1-1")] + [DataRow("#12345G")] + [DataRow("#FFFFFF+F")] + [DataRow("#F F")] + [DataRow("#G00")] + public void FromHex_NonHexCharacter_ThrowsArgumentException(string hex) => + Assert.ThrowsExactly(() => Color.FromHex(hex)); + + [TestMethod] + [DataRow("#abc", "#AABBCC")] + [DataRow("#ABC", "#AABBCC")] + [DataRow("#3a7bd5", "#3A7BD5")] + [DataRow("3A7BD5", "#3A7BD5")] + [DataRow("#3a7bd580", "#3A7BD580")] + public void FromHex_ValidDigitsInEitherCase_RoundTrip(string hex, string expected) => + Assert.AreEqual(expected, Color.FromHex(hex).ToHex()); }