From 2bd9b77701588af55774a19b6017d29ae4727394 Mon Sep 17 00:00:00 2001 From: Craig Fowler Date: Fri, 22 May 2026 08:01:11 +0100 Subject: [PATCH 1/5] Resolve #56 - Comparing cross-types --- .../BrowserVersionIntegrationTests.cs | 14 +++++++++ .../Identification/BrowserVersion.cs | 2 +- ...sion.cs => DottedNumericBrowserVersion.cs} | 18 +++++++++-- .../Identification/SemanticBrowserVersion.cs | 31 ++++++++++++++++--- 4 files changed, 57 insertions(+), 8 deletions(-) create mode 100644 CSF.Extensions.WebDriver.Tests/Identification/BrowserVersionIntegrationTests.cs rename CSF.Extensions.WebDriver/Identification/{DottedNumericVersion.cs => DottedNumericBrowserVersion.cs} (80%) diff --git a/CSF.Extensions.WebDriver.Tests/Identification/BrowserVersionIntegrationTests.cs b/CSF.Extensions.WebDriver.Tests/Identification/BrowserVersionIntegrationTests.cs new file mode 100644 index 0000000..fc400d1 --- /dev/null +++ b/CSF.Extensions.WebDriver.Tests/Identification/BrowserVersionIntegrationTests.cs @@ -0,0 +1,14 @@ +namespace CSF.Extensions.WebDriver.Identification; + +[TestFixture, Parallelizable] +public class BrowserVersionIntegrationTests +{ + [Test] + public void AHigherVersionShouldBeGreaterThanALowerOneWithMoreComponents() + { + var first = BrowserVersion.Create("95.0.4638.54"); + var second = BrowserVersion.Create("95.1"); + + Assert.That(first, Is.LessThan(second), "First result is less than second result"); + } +} \ No newline at end of file diff --git a/CSF.Extensions.WebDriver/Identification/BrowserVersion.cs b/CSF.Extensions.WebDriver/Identification/BrowserVersion.cs index 80a387c..731cf56 100644 --- a/CSF.Extensions.WebDriver/Identification/BrowserVersion.cs +++ b/CSF.Extensions.WebDriver/Identification/BrowserVersion.cs @@ -166,7 +166,7 @@ public static BrowserVersion Create(string version, string requestedVersion = nu if (SemanticBrowserVersion.TryParse(version, out var semVersion)) return semVersion; if (DottedNumericBrowserVersion.TryParse(version, out var numericVersion)) return numericVersion; if (SemanticBrowserVersion.TryParse(requestedVersion, out var requestedSemVersion, true)) return requestedSemVersion; - if (DottedNumericBrowserVersion.TryParse(requestedVersion, out var requestedNumericVersion)) return requestedNumericVersion; + if (DottedNumericBrowserVersion.TryParse(requestedVersion, out var requestedNumericVersion, true)) return requestedNumericVersion; if (UnrecognisedBrowserVersion.TryParse(version, out var unrecognisedVersion)) return unrecognisedVersion; if (UnrecognisedBrowserVersion.TryParse(requestedVersion, out var requestedUnrecognisedVersion)) return requestedUnrecognisedVersion; diff --git a/CSF.Extensions.WebDriver/Identification/DottedNumericVersion.cs b/CSF.Extensions.WebDriver/Identification/DottedNumericBrowserVersion.cs similarity index 80% rename from CSF.Extensions.WebDriver/Identification/DottedNumericVersion.cs rename to CSF.Extensions.WebDriver/Identification/DottedNumericBrowserVersion.cs index d815220..85f5b00 100644 --- a/CSF.Extensions.WebDriver/Identification/DottedNumericVersion.cs +++ b/CSF.Extensions.WebDriver/Identification/DottedNumericBrowserVersion.cs @@ -16,6 +16,12 @@ namespace CSF.Extensions.WebDriver.Identification /// It permits any amount of leading and trailing non-numeric characters /// It permits any number of 'version' components, not just a maximum of 3 as is the case with SemVer /// + /// + /// The implementations of and include special-case logic for comparing/equating + /// dotted numeric versions with instances. If these methods (from this type) are used with a semantic version + /// then that semantic version is converted into a dotted numeric version first, using . + /// The methods then proceed according to their usual logic, with the resulting converted version. + /// /// public sealed class DottedNumericBrowserVersion : BrowserVersion { @@ -33,8 +39,13 @@ public sealed class DottedNumericBrowserVersion : BrowserVersion /// public override int CompareTo(BrowserVersion other) { - if (other is null || !(other is DottedNumericBrowserVersion version)) return 1; + if(other is DottedNumericBrowserVersion dotVersion) return CompareTo(dotVersion); + if(other is SemanticBrowserVersion semVersion) return CompareTo(semVersion.ToDottedNumericBrowserVersion()); + return 1; + } + int CompareTo(DottedNumericBrowserVersion version) + { var theirCount = version.VersionComponents.Count; for (var i = 0; i < VersionComponents.Count; i++) { @@ -55,8 +66,9 @@ public override int CompareTo(BrowserVersion other) /// public override bool Equals(BrowserVersion other) { - if (other is null || !(other is DottedNumericBrowserVersion version)) return false; - return VersionComponents.SequenceEqual(version.VersionComponents); + if(other is DottedNumericBrowserVersion dotVersion) return VersionComponents.SequenceEqual(dotVersion.VersionComponents); + if(other is SemanticBrowserVersion semVersion) return VersionComponents.SequenceEqual(semVersion.ToDottedNumericBrowserVersion().VersionComponents); + return false; } /// diff --git a/CSF.Extensions.WebDriver/Identification/SemanticBrowserVersion.cs b/CSF.Extensions.WebDriver/Identification/SemanticBrowserVersion.cs index 8433b39..cc496d8 100644 --- a/CSF.Extensions.WebDriver/Identification/SemanticBrowserVersion.cs +++ b/CSF.Extensions.WebDriver/Identification/SemanticBrowserVersion.cs @@ -17,6 +17,13 @@ namespace CSF.Extensions.WebDriver.Identification /// to permit some common improper representations of a semantic version. The TryParse function in this class /// uses to enable very generous parsing. /// + /// + /// The implementations of and include special-case logic for comparing/equating + /// semantic versions with instances. If these methods (from this type) are used with a dotted numeric version + /// then the current instance is converted into a dotted numeric version first, using . + /// The equality/comparison methods then make use of and + /// accordingly, performing using the comparison functions from the converted version instead. + /// /// public sealed class SemanticBrowserVersion : BrowserVersion { @@ -28,17 +35,33 @@ public sealed class SemanticBrowserVersion : BrowserVersion /// public override int CompareTo(BrowserVersion other) { - if (other is null || !(other is SemanticBrowserVersion semVersion)) return 1; - return Version.CompareSortOrderTo(semVersion.Version); + if(other is SemanticBrowserVersion semVersion) return Version.CompareSortOrderTo(semVersion.Version); + if(other is DottedNumericBrowserVersion dotVersion) return ToDottedNumericBrowserVersion().CompareTo(dotVersion); + return 1; } /// public override bool Equals(BrowserVersion other) { - if (other is null || !(other is SemanticBrowserVersion semVersion)) return false; - return Version.Equals(semVersion.Version); + if(other is SemanticBrowserVersion semVersion) return Version.Equals(semVersion.Version); + if(other is DottedNumericBrowserVersion dotVersion) return ToDottedNumericBrowserVersion().Equals(dotVersion); + return false; } + /// + /// Converts the current into an instance of . + /// + /// + /// + /// This is useful in situations where the current version must be compared with a dotted numeric version. + /// Note that only the Major, Minor and Patch version components are converted. Any prerelease information or build + /// metadata are omitted from this conversion process. + /// + /// + /// A dotted numeric browser version, created from the major, minor and patch components of this semantic version. + public DottedNumericBrowserVersion ToDottedNumericBrowserVersion() + => new DottedNumericBrowserVersion(new [] {Version.Major, Version.Minor, Version.Patch}, IsPresumedVersion); + /// public override int GetHashCode() => Version.GetHashCode(); From 1295084909519fecd8107b5b640e519b6c4244a5 Mon Sep 17 00:00:00 2001 From: Craig Fowler Date: Fri, 22 May 2026 13:23:21 +0100 Subject: [PATCH 2/5] Improve coverage for #56 Also undo the equality stuff, that's not desirable. --- .../BrowserVersionIntegrationTests.cs | 33 +++++++++++++++++-- .../DottedNumericBrowserVersion.cs | 6 +--- .../Identification/SemanticBrowserVersion.cs | 6 +--- 3 files changed, 32 insertions(+), 13 deletions(-) diff --git a/CSF.Extensions.WebDriver.Tests/Identification/BrowserVersionIntegrationTests.cs b/CSF.Extensions.WebDriver.Tests/Identification/BrowserVersionIntegrationTests.cs index fc400d1..76fe0c2 100644 --- a/CSF.Extensions.WebDriver.Tests/Identification/BrowserVersionIntegrationTests.cs +++ b/CSF.Extensions.WebDriver.Tests/Identification/BrowserVersionIntegrationTests.cs @@ -3,12 +3,39 @@ namespace CSF.Extensions.WebDriver.Identification; [TestFixture, Parallelizable] public class BrowserVersionIntegrationTests { + [Test] + public void ALowerVersionShouldBeLessThanAHigherOneWithMoreComponents() + { + var first = BrowserVersion.Create("95.0.4638"); + var second = BrowserVersion.Create("95.1.1234.5678"); + + Assert.That(first, Is.LessThan(second), "First version is less than second version"); + } + [Test] public void AHigherVersionShouldBeGreaterThanALowerOneWithMoreComponents() { - var first = BrowserVersion.Create("95.0.4638.54"); + var first = BrowserVersion.Create("95.2.4638"); + var second = BrowserVersion.Create("95.1.1234.5678"); + + Assert.That(first, Is.GreaterThan(second), "First version is greater than second version"); + } + + [Test] + public void ALowerVersionShouldBeLessThanAHigherOneWithFewerComponents() + { + var first = BrowserVersion.Create("95.0.4638.1234"); + var second = BrowserVersion.Create("95.1"); + + Assert.That(first, Is.LessThan(second), "First version is less than second version"); + } + + [Test] + public void AHigherVersionShouldBeGreaterThanALowerOneWithFewerComponents() + { + var first = BrowserVersion.Create("95.2.4638.1234"); var second = BrowserVersion.Create("95.1"); - Assert.That(first, Is.LessThan(second), "First result is less than second result"); - } + Assert.That(first, Is.GreaterThan(second), "First version is greater than second version"); + } } \ No newline at end of file diff --git a/CSF.Extensions.WebDriver/Identification/DottedNumericBrowserVersion.cs b/CSF.Extensions.WebDriver/Identification/DottedNumericBrowserVersion.cs index 85f5b00..df7f2f6 100644 --- a/CSF.Extensions.WebDriver/Identification/DottedNumericBrowserVersion.cs +++ b/CSF.Extensions.WebDriver/Identification/DottedNumericBrowserVersion.cs @@ -65,11 +65,7 @@ int CompareTo(DottedNumericBrowserVersion version) /// public override bool Equals(BrowserVersion other) - { - if(other is DottedNumericBrowserVersion dotVersion) return VersionComponents.SequenceEqual(dotVersion.VersionComponents); - if(other is SemanticBrowserVersion semVersion) return VersionComponents.SequenceEqual(semVersion.ToDottedNumericBrowserVersion().VersionComponents); - return false; - } + => other is DottedNumericBrowserVersion dotVersion && VersionComponents.SequenceEqual(dotVersion.VersionComponents); /// public override int GetHashCode() => VersionComponents.Aggregate(17, HashFunction); diff --git a/CSF.Extensions.WebDriver/Identification/SemanticBrowserVersion.cs b/CSF.Extensions.WebDriver/Identification/SemanticBrowserVersion.cs index cc496d8..0fb8cab 100644 --- a/CSF.Extensions.WebDriver/Identification/SemanticBrowserVersion.cs +++ b/CSF.Extensions.WebDriver/Identification/SemanticBrowserVersion.cs @@ -42,11 +42,7 @@ public override int CompareTo(BrowserVersion other) /// public override bool Equals(BrowserVersion other) - { - if(other is SemanticBrowserVersion semVersion) return Version.Equals(semVersion.Version); - if(other is DottedNumericBrowserVersion dotVersion) return ToDottedNumericBrowserVersion().Equals(dotVersion); - return false; - } + => other is SemanticBrowserVersion semVersion && Version.Equals(semVersion.Version); /// /// Converts the current into an instance of . From a73c628017f3206c05c63d89036d9e89b88d1d00 Mon Sep 17 00:00:00 2001 From: Craig Fowler Date: Fri, 22 May 2026 13:32:34 +0100 Subject: [PATCH 3/5] Fix 2 tech issues --- .../Identification/DottedNumericBrowserVersion.cs | 3 +++ .../Identification/SemanticBrowserVersion.cs | 3 +++ 2 files changed, 6 insertions(+) diff --git a/CSF.Extensions.WebDriver/Identification/DottedNumericBrowserVersion.cs b/CSF.Extensions.WebDriver/Identification/DottedNumericBrowserVersion.cs index df7f2f6..734c96f 100644 --- a/CSF.Extensions.WebDriver/Identification/DottedNumericBrowserVersion.cs +++ b/CSF.Extensions.WebDriver/Identification/DottedNumericBrowserVersion.cs @@ -72,6 +72,9 @@ public override bool Equals(BrowserVersion other) static int HashFunction(int acc, int next) { unchecked { return acc * 23 + next; } } + /// + public override bool Equals(object obj) => obj is BrowserVersion ver && Equals(ver); + /// public override string ToString() => string.Join(".", VersionComponents.Select(x => x.ToString())) + PresumedSuffix; diff --git a/CSF.Extensions.WebDriver/Identification/SemanticBrowserVersion.cs b/CSF.Extensions.WebDriver/Identification/SemanticBrowserVersion.cs index 0fb8cab..7498278 100644 --- a/CSF.Extensions.WebDriver/Identification/SemanticBrowserVersion.cs +++ b/CSF.Extensions.WebDriver/Identification/SemanticBrowserVersion.cs @@ -60,6 +60,9 @@ public DottedNumericBrowserVersion ToDottedNumericBrowserVersion() /// public override int GetHashCode() => Version.GetHashCode(); + + /// + public override bool Equals(object obj) => obj is BrowserVersion ver && Equals(ver); /// public override string ToString() => Version.ToString() + PresumedSuffix; From a783dc60e215d21d7af5b9f6044960cdd05fcff8 Mon Sep 17 00:00:00 2001 From: Craig Fowler Date: Fri, 22 May 2026 13:34:18 +0100 Subject: [PATCH 4/5] Fix security issue ReDoS --- .../Identification/DottedNumericBrowserVersion.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CSF.Extensions.WebDriver/Identification/DottedNumericBrowserVersion.cs b/CSF.Extensions.WebDriver/Identification/DottedNumericBrowserVersion.cs index 734c96f..bf286ad 100644 --- a/CSF.Extensions.WebDriver/Identification/DottedNumericBrowserVersion.cs +++ b/CSF.Extensions.WebDriver/Identification/DottedNumericBrowserVersion.cs @@ -26,7 +26,7 @@ namespace CSF.Extensions.WebDriver.Identification public sealed class DottedNumericBrowserVersion : BrowserVersion { const string parserPattern = @"(\d+)(?:\.(\d+))*"; - static readonly Regex parser = new Regex(parserPattern, RegexOptions.Compiled | RegexOptions.CultureInvariant); + static readonly Regex parser = new Regex(parserPattern, RegexOptions.Compiled | RegexOptions.CultureInvariant, TimeSpan.FromMilliseconds(50)); readonly IReadOnlyList components; From 2d0346e862c200cf622451b3872b01a18a3e622a Mon Sep 17 00:00:00 2001 From: Craig Fowler Date: Fri, 22 May 2026 18:30:00 +0100 Subject: [PATCH 5/5] #56 - Add some tests --- .../DottedNumericBrowserVersionTests.cs | 32 +++++++++++++++++++ .../DottedNumericBrowserVersion.cs | 6 ++-- .../Identification/SemanticBrowserVersion.cs | 6 ++-- 3 files changed, 38 insertions(+), 6 deletions(-) diff --git a/CSF.Extensions.WebDriver.Tests/Identification/DottedNumericBrowserVersionTests.cs b/CSF.Extensions.WebDriver.Tests/Identification/DottedNumericBrowserVersionTests.cs index ca3d1fa..c235347 100644 --- a/CSF.Extensions.WebDriver.Tests/Identification/DottedNumericBrowserVersionTests.cs +++ b/CSF.Extensions.WebDriver.Tests/Identification/DottedNumericBrowserVersionTests.cs @@ -61,4 +61,36 @@ public void TryParseShouldReturnFalseForAnInvalidVersion() { Assert.That(DottedNumericBrowserVersion.TryParse("Elephants", out _), Is.False); } + + [Test, AutoMoqData] + public void GetHashCodeShouldReturnTheSameResultForTwoEqualInstances() + { + var one = new DottedNumericBrowserVersion([1, 2, 3]); + var two = new DottedNumericBrowserVersion([1, 2, 3]); + + Assert.That(one.GetHashCode(), Is.EqualTo(two.GetHashCode())); + } + + [Test, AutoMoqData] + public void EqualsObjectShouldReturnTrueForTwoEqualInstances() + { + object one = new DottedNumericBrowserVersion([1, 2, 3]); + object two = new DottedNumericBrowserVersion([1, 2, 3]); + +#pragma warning disable NUnit2010 // Use EqualConstraint - not doing this to explicitly show what I'm testing + Assert.That(one.Equals(two), Is.True); +#pragma warning restore NUnit2010 + } + + [Test, AutoMoqData] + public void ConstructorShouldThrowForAnEmptyListOfComponents() + { + Assert.That(() => new DottedNumericBrowserVersion([]), Throws.ArgumentException); + } + + [Test, AutoMoqData] + public void ConstructorShouldThrowForANullListOfComponents() + { + Assert.That(() => new DottedNumericBrowserVersion(null), Throws.ArgumentNullException); + } } \ No newline at end of file diff --git a/CSF.Extensions.WebDriver/Identification/DottedNumericBrowserVersion.cs b/CSF.Extensions.WebDriver/Identification/DottedNumericBrowserVersion.cs index bf286ad..836e222 100644 --- a/CSF.Extensions.WebDriver/Identification/DottedNumericBrowserVersion.cs +++ b/CSF.Extensions.WebDriver/Identification/DottedNumericBrowserVersion.cs @@ -67,14 +67,14 @@ int CompareTo(DottedNumericBrowserVersion version) public override bool Equals(BrowserVersion other) => other is DottedNumericBrowserVersion dotVersion && VersionComponents.SequenceEqual(dotVersion.VersionComponents); + /// + public override bool Equals(object obj) => obj is BrowserVersion ver && Equals(ver); + /// public override int GetHashCode() => VersionComponents.Aggregate(17, HashFunction); static int HashFunction(int acc, int next) { unchecked { return acc * 23 + next; } } - /// - public override bool Equals(object obj) => obj is BrowserVersion ver && Equals(ver); - /// public override string ToString() => string.Join(".", VersionComponents.Select(x => x.ToString())) + PresumedSuffix; diff --git a/CSF.Extensions.WebDriver/Identification/SemanticBrowserVersion.cs b/CSF.Extensions.WebDriver/Identification/SemanticBrowserVersion.cs index 7498278..34d38c5 100644 --- a/CSF.Extensions.WebDriver/Identification/SemanticBrowserVersion.cs +++ b/CSF.Extensions.WebDriver/Identification/SemanticBrowserVersion.cs @@ -43,6 +43,9 @@ public override int CompareTo(BrowserVersion other) /// public override bool Equals(BrowserVersion other) => other is SemanticBrowserVersion semVersion && Version.Equals(semVersion.Version); + + /// + public override bool Equals(object obj) => obj is BrowserVersion ver && Equals(ver); /// /// Converts the current into an instance of . @@ -60,9 +63,6 @@ public DottedNumericBrowserVersion ToDottedNumericBrowserVersion() /// public override int GetHashCode() => Version.GetHashCode(); - - /// - public override bool Equals(object obj) => obj is BrowserVersion ver && Equals(ver); /// public override string ToString() => Version.ToString() + PresumedSuffix;