• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

KSP-CKAN / CKAN / 29550533164

17 Jul 2026 02:41AM UTC coverage: 87.981% (+0.1%) from 87.84%
29550533164

Pull #4694

github

web-flow
Merge 167d5746a into 0daeea8d7
Pull Request #4694: New URL protocol and handler improvements

2037 of 2161 branches covered (94.26%)

Branch coverage included in aggregate %.

80 of 86 new or added lines in 2 files covered. (93.02%)

34 existing lines in 4 files now uncovered.

8724 of 10070 relevant lines covered (86.63%)

1.81 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.1
/Core/Versioning/ModuleVersion.cs
1
using System;
2
using System.Collections.Concurrent;
3
using System.Text.RegularExpressions;
4
using Newtonsoft.Json;
5

6
namespace CKAN.Versioning
7
{
8
    /// <summary>
9
    /// Represents the version number of a module.
10
    /// </summary>
11
    /// <remarks>
12
    /// <para>
13
    /// The format of the version number is as follows. Optional components are shown in square brackets
14
    /// (<c>[</c> and <c>]</c>):
15
    /// </para>
16
    /// <code>
17
    /// [epoch:]version
18
    /// </code>
19
    /// <para>
20
    /// <c>epoch</c> must be an integer greater than or equal to 0. If not present it is assumed to be 0.
21
    /// <c>version</c> may be any arbitrary string.
22
    /// </para>
23
    /// </remarks>
24
    [Serializable]
25
    [JsonConverter(typeof(JsonSimpleStringConverter))]
26
    public partial class ModuleVersion
27
    {
28
        private static readonly Regex Pattern =
2✔
29
            new Regex(@"^(?:(?<epoch>[0-9]+):)?(?<version>.*)$", RegexOptions.Compiled);
30

31
        private static readonly ConcurrentDictionary<Tuple<string, string>, int> ComparisonCache =
2✔
32
            new ConcurrentDictionary<Tuple<string, string>, int>();
33
    }
34

35
    public partial class ModuleVersion
36
    {
37
        private readonly int _epoch;
38
        private readonly string _version;
39
        private readonly string _string;
40

41
        /// <summary>
42
        /// Initializes a new instance of the <see cref="ModuleVersion"/> class using the specified string.
43
        /// </summary>
44
        /// <param name="version">A <see cref="string"/> in the appropriate format.</param>
45
        public ModuleVersion(string version)
2✔
46
        {
47
            var match = Pattern.Match(version);
2✔
48

49
            if (!match.Success)
2✔
50
            {
51
                throw new FormatException("Input string was not in a correct format.");
×
52
            }
53

54
            // If we have an epoch, then record it.
55
            if (match.Groups["epoch"].Value.Length > 0)
2✔
56
            {
57
                _epoch = Convert.ToInt32( match.Groups["epoch"].Value );
2✔
58
            }
59

60
            _version = match.Groups["version"].Value;
2✔
61
            _string = version;
2✔
62
        }
2✔
63

64
        /// <returns>
65
        /// true if versions have the same epoch, false if different
66
        /// </returns>
67
        public bool EpochEquals(ModuleVersion other)
68
            => _epoch == other._epoch;
2✔
69

70
        /// <returns>
71
        /// New module version with same version as 'this' but with one greater epoch
72
        /// </returns>
73
        public ModuleVersion IncrementEpoch()
74
            => new ModuleVersion($"{_epoch + 1}:{_version}");
2✔
75

76
        /// <summary>
77
        /// Converts the value of the current <see cref="ModuleVersion"/> object to its equivalent
78
        /// <see cref="string"/> representation.
79
        /// </summary>
80
        /// <returns>
81
        /// The <see cref="string"/> representation of the current <see cref="ModuleVersion"/> object.
82
        /// </returns>
83
        /// /// <remarks>
84
        /// The return value should not be considered safe for use in file paths.
85
        /// </remarks>
86
        public override string ToString()
87
            => _string;
2✔
88

89
        public string ToString(bool hideEpoch, bool hideV)
90
            => hideEpoch
2✔
91
                ? hideV
92
                    ? StripEpoch(StripV(_string))
93
                    : StripEpoch(_string)
94
                : hideV
95
                    ? StripV(_string)
96
                    : _string;
97

98
        /// <summary>
99
        /// Remove prepending v V. Version_ etc
100
        /// </summary>
101
        private static string StripV(string version)
102
            => Regex.Match(version, @"^(?<num>\d\:)?[vV]+(ersion)?[_.]*(?<ver>\d.*)$") is Match match
2✔
103
               && match.Success
104
                   ? match.Groups["num"].Value + match.Groups["ver"].Value
105
                   : version;
106

107
        /// <summary>
108
        /// Returns a version string shorn of any leading epoch as delimited by a single colon
109
        /// </summary>
110
        /// <param name="version">A version string that might contain an epoch</param>
111
        private static string StripEpoch(string version)
112
            // If our version number starts with a string of digits, followed by
113
            // a colon, and then has no more colons, we're probably safe to assume
114
            // the first string of digits is an epoch
115
            => epochMatch.IsMatch(version)
2✔
116
                ? epochReplace.Replace(version, @"$2")
117
                : version;
118

119
        /// <summary>
120
        /// As above, but includes the original in parentheses
121
        /// </summary>
122
        public string WithAndWithoutEpoch()
123
            => epochMatch.IsMatch(_string)
2✔
124
                ? $"{epochReplace.Replace(_string, @"$2")} ({_string})"
125
                : _string;
126

127
        private static readonly Regex epochMatch   = new Regex(@"^[0-9][0-9]*:[^:]+$", RegexOptions.Compiled);
2✔
128
        private static readonly Regex epochReplace = new Regex(@"^([^:]+):([^:]+)$",   RegexOptions.Compiled);
2✔
129
    }
130

131
    public partial class ModuleVersion : IEquatable<ModuleVersion>
132
    {
133
        public override bool Equals(object? obj)
134
            => ReferenceEquals(this, obj)
2✔
135
                || (obj is ModuleVersion version && Equals(version));
136

137
        public bool Equals(ModuleVersion? other)
138
            => ReferenceEquals(this, other)
2✔
139
                || CompareTo(other) == 0;
140

141
        public override int GetHashCode()
142
            => (_epoch, _version).GetHashCode();
2✔
143

144
        /// <summary>
145
        /// Compares two <see cref="ModuleVersion"/> objects to determine if the first is equal to the second.
146
        /// </summary>
147
        /// <param name="left">The first of two <see cref="ModuleVersion"/> objects to compare.</param>
148
        /// <param name="right">The second of two <see cref="ModuleVersion"/> objects to compare.</param>
149
        /// <returns>
150
        /// <list type="bullet">
151
        /// <item>
152
        /// <term><c>true</c></term>
153
        /// <description>
154
        /// When <paramref name="left"/> is equal to <paramref name="right"/>.
155
        /// </description>
156
        /// </item>
157
        /// <item>
158
        /// <term><c>false</c></term>
159
        /// <description>
160
        /// When <paramref name="left"/> is not equal to <paramref name="right"/>.
161
        /// </description>
162
        /// </item>
163
        /// </list>
164
        /// </returns>
165
        public static bool operator ==(ModuleVersion? left, ModuleVersion? right)
166
            => Equals(left, right);
2✔
167

168
        /// <summary>
169
        /// Compares two <see cref="ModuleVersion"/> objects to determine if the first is not equal to the second.
170
        /// </summary>
171
        /// <param name="left">The first of two <see cref="ModuleVersion"/> objects to compare.</param>
172
        /// <param name="right">The second of two <see cref="ModuleVersion"/> objects to compare.</param>
173
        /// <returns>
174
        /// <list type="bullet">
175
        /// <item>
176
        /// <term><c>true</c></term>
177
        /// <description>
178
        /// When <paramref name="left"/> is not equal to <paramref name="right"/>.
179
        /// </description>
180
        /// </item>
181
        /// <item>
182
        /// <term><c>false</c></term>
183
        /// <description>
184
        /// When <paramref name="left"/> is equal to <paramref name="right"/>.
185
        /// </description>
186
        /// </item>
187
        /// </list>
188
        /// </returns>
189
        public static bool operator !=(ModuleVersion? left, ModuleVersion? right)
190
            => !Equals(left, right);
2✔
191
    }
192

193
    public partial class ModuleVersion : IComparable<ModuleVersion>
194
    {
195
        /// <summary>
196
        /// Compares the current <see cref="ModuleVersion"/> object to a specified <see cref="ModuleVersion"/> object
197
        /// and returns an indication of their relative values.
198
        /// </summary>
199
        /// <param name="other">
200
        /// A <see cref="ModuleVersion"/> object to compare to the current <see cref="ModuleVersion"/> object.
201
        /// </param>
202
        /// <returns>
203
        /// <list type="bullet">
204
        /// <item>
205
        /// <term>A negative value</term>
206
        /// <description>
207
        /// When the current <see cref="ModuleVersion"/> object is less than the specified <see cref="ModuleVersion"/>
208
        /// object.
209
        /// </description>
210
        /// </item>
211
        /// <item>
212
        /// <term>Zero</term>
213
        /// <description>
214
        /// When the current <see cref="ModuleVersion"/> object is equal to the specified <see cref="ModuleVersion"/>
215
        /// object.
216
        /// </description>
217
        /// </item>
218
        /// <item>
219
        /// <term>A positive value</term>
220
        /// <description>
221
        /// When the current <see cref="ModuleVersion"/> object is greater than the specified
222
        /// <see cref="ModuleVersion"/> object.
223
        /// </description>
224
        /// </item>
225
        /// </list>
226
        /// </returns>
227
        public int CompareTo(ModuleVersion? other)
228
        {
229
            Comparison stringComp(string v1, string v2)
230
            {
231
                var comparison = new Comparison { FirstRemainder = "", SecondRemainder = "" };
2✔
232

233
                // Our starting assumptions are that both versions are completely
234
                // strings, with no remainder. We'll then check if they're not.
235

236
                var str1 = v1;
2✔
237
                var str2 = v2;
2✔
238

239
                // Start by walking along our version string until we find a number,
240
                // thereby finding the starting string in both cases. If we fall off
241
                // the end, then our assumptions made above hold.
242

243
                for (var i = 0; i < v1.Length; i++)
6✔
244
                {
245
                    if (char.IsNumber(v1[i]))
2✔
246
                    {
247
                        comparison.FirstRemainder = v1[i..];
2✔
248
                        str1 = v1[..i];
2✔
249
                        break;
2✔
250
                    }
251
                }
252

253
                for (var i = 0; i < v2.Length; i++)
6✔
254
                {
255
                    if (char.IsNumber(v2[i]))
2✔
256
                    {
257
                        comparison.SecondRemainder = v2[i..];
2✔
258
                        str2 = v2[..i];
2✔
259
                        break;
2✔
260
                    }
261
                }
262

263
                // Then compare the two strings, and return our comparison state.
264
                // Override sorting of '.' to higher than other characters.
265
                if (//str1 is [char first1, ..] && str2 is [char first2, ..]
2✔
266
                    str1.Length > 0 && str1[0] is var first1
267
                 && str2.Length > 0 && str2[0] is var first2)
268
                {
269
                    if (first1 != '.' && first2 == '.')
2✔
270
                    {
271
                        comparison.CompareTo = -1;
2✔
272
                    }
273
                    else if (first1 == '.' && first2 != '.')
2✔
274
                    {
275
                        comparison.CompareTo = 1;
2✔
276
                    }
277
                    else if (first1 == '.' && first2 == '.')
2✔
278
                    {
279
                        if (str1.Length == 1 && str2.Length > 1)
2✔
280
                        {
281
                            comparison.CompareTo = 1;
2✔
282
                        }
283
                        else if (str1.Length > 1 && str2.Length == 1)
2✔
284
                        {
285
                            comparison.CompareTo = -1;
2✔
286
                        }
287
                    }
288
                    else
289
                    {
290
                        comparison.CompareTo = string.CompareOrdinal(str1, str2);
2✔
291
                    }
292
                }
293
                else
294
                {
295
                    comparison.CompareTo = string.CompareOrdinal(str1, str2);
2✔
296
                }
297
                return comparison;
2✔
298
            }
299

300
            Comparison numComp(string v1, string v2)
301
            {
302
                var comparison = new Comparison { FirstRemainder = "", SecondRemainder = "" };
2✔
303

304
                var minimumLength1 = 0;
2✔
305
                for (var i = 0; i < v1.Length; i++)
6✔
306
                {
307
                    if (!char.IsNumber(v1[i]))
2✔
308
                    {
309
                        comparison.FirstRemainder = v1[i..];
2✔
310
                        break;
2✔
311
                    }
312

313
                    minimumLength1++;
2✔
314
                }
315

316
                var minimumLength2 = 0;
2✔
317
                for (var i = 0; i < v2.Length; i++)
6✔
318
                {
319
                    if (!char.IsNumber(v2[i]))
2✔
320
                    {
321
                        comparison.SecondRemainder = v2[i..];
2✔
322
                        break;
2✔
323
                    }
324

325
                    minimumLength2++;
2✔
326
                }
327

328

329
                if (!int.TryParse(v1[..minimumLength1], out var integer1))
2✔
330
                {
UNCOV
331
                    integer1 = 0;
×
332
                }
333

334
                if (!int.TryParse(v2[..minimumLength2], out var integer2))
2✔
335
                {
UNCOV
336
                    integer2 = 0;
×
337
                }
338

339
                comparison.CompareTo = integer1.CompareTo(integer2);
2✔
340
                return comparison;
2✔
341
            }
342

343
            if (other == null)
2✔
344
            {
UNCOV
345
                throw new ArgumentNullException(nameof(other));
×
346
            }
347

348
            if (other._epoch == _epoch && other._version.Equals(_version))
2✔
349
            {
350
                return 0;
2✔
351
            }
352

353
            // Compare epochs first.
354
            if (_epoch != other._epoch)
2✔
355
            {
356
                return _epoch > other._epoch ? 1 : -1;
2✔
357
            }
358

359
            // Epochs are the same. Do the dance described in
360
            // https://github.com/KSP-CKAN/CKAN/blob/master/Spec.md#version-ordering
361
            var tuple = new Tuple<string, string>(_string, other._string);
2✔
362
            if (ComparisonCache.TryGetValue(tuple, out var ret))
2✔
363
            {
364
                return ret;
2✔
365
            }
366

367
            Comparison comp;
368
            comp.FirstRemainder = _version;
2✔
369
            comp.SecondRemainder = other._version;
2✔
370

371
            // Process our strings while there are characters remaining
372
            while (comp.FirstRemainder.Length > 0 && comp.SecondRemainder.Length > 0)
2✔
373
            {
374
                // Start by comparing the string parts.
375
                comp = stringComp(comp.FirstRemainder, comp.SecondRemainder);
2✔
376

377
                // If we've found a difference, return it.
378
                if (comp.CompareTo != 0)
2✔
379
                {
380
                    ComparisonCache.TryAdd(tuple, comp.CompareTo);
2✔
381
                    return comp.CompareTo;
2✔
382
                }
383

384
                // Otherwise, compare the number parts.
385
                // It's okay not to check if our strings are exhausted, because
386
                // if they are the exhausted parts will return zero.
387

388
                comp = numComp(comp.FirstRemainder, comp.SecondRemainder);
2✔
389

390
                // Again, return difference if found.
391
                if (comp.CompareTo != 0)
2✔
392
                {
393
                    ComparisonCache.TryAdd(tuple, comp.CompareTo);
2✔
394
                    return comp.CompareTo;
2✔
395
                }
396
            }
397

398
            // Oh, we've run out of one or both strings.
399

400
            if (comp.FirstRemainder.Length == 0)
2✔
401
            {
402
                if (comp.SecondRemainder.Length == 0)
2✔
403
                {
404
                    ComparisonCache.TryAdd(tuple, 0);
2✔
405
                    return 0;
2✔
406
                }
407

408
                // They *can't* be equal, because we would have detected that in our first test.
409
                // So, whichever version is empty first is the smallest. (1.2 < 1.2.3)
410
                ComparisonCache.TryAdd(tuple, -1);
2✔
411
                return -1;
2✔
412
            }
413
            ComparisonCache.TryAdd(tuple, 1);
2✔
414
            return 1;
2✔
415
        }
416

417
        /// <summary>
418
        /// Compares the current <see cref="ModuleVersion"/> object to a specified <see cref="ModuleVersion"/> object
419
        /// and returns if it is less than the other object.
420
        /// </summary>
421
        /// <param name="other">
422
        /// A <see cref="ModuleVersion"/> object to compare to the current <see cref="ModuleVersion"/> object, or
423
        /// <c>null</c>.
424
        /// </param>
425
        /// <returns>
426
        /// <list type="bullet">
427
        /// <item>
428
        /// <term><c>true</c></term>
429
        /// <description>
430
        /// When the current <see cref="ModuleVersion"/> object is less than the specified <see cref="ModuleVersion"/>
431
        /// object.
432
        /// </description>
433
        /// </item>
434
        /// <item>
435
        /// <term><c>false</c></term>
436
        /// <description>
437
        /// When the current <see cref="ModuleVersion"/> object is not less than the specified
438
        /// <see cref="ModuleVersion"/> object.
439
        /// </description>
440
        /// </item>
441
        /// </list>
442
        /// </returns>
443
        public bool IsLessThan(ModuleVersion? other)
444
            => CompareTo(other) < 0;
2✔
445

446
        /// <summary>
447
        /// Compares the current <see cref="ModuleVersion"/> object to a specified <see cref="ModuleVersion"/> object
448
        /// and returns if it is greater than the other object.
449
        /// </summary>
450
        /// <param name="other">
451
        /// A <see cref="ModuleVersion"/> object to compare to the current <see cref="ModuleVersion"/> object, or
452
        /// <c>null</c>.
453
        /// </param>
454
        /// <returns>
455
        /// <list type="bullet">
456
        /// <item>
457
        /// <term><c>true</c></term>
458
        /// <description>
459
        /// When the current <see cref="ModuleVersion"/> object is greater than the specified
460
        /// <see cref="ModuleVersion"/> object.
461
        /// </description>
462
        /// </item>
463
        /// <item>
464
        /// <term><c>false</c></term>
465
        /// <description>
466
        /// When the current <see cref="ModuleVersion"/> object is not greater than the specified
467
        /// <see cref="ModuleVersion"/> object.
468
        /// </description>
469
        /// </item>
470
        /// </list>
471
        /// </returns>
472
        public bool IsGreaterThan(ModuleVersion other)
473
            => CompareTo(other) > 0;
2✔
474

475
        /// <summary>
476
        /// Compares two <see cref="ModuleVersion"/> objects to determine if the first is less than the second.
477
        /// </summary>
478
        /// <param name="ver1">The first of two <see cref="ModuleVersion"/> objects to compare.</param>
479
        /// <param name="ver2">The second of two <see cref="ModuleVersion"/> objects to compare.</param>
480
        /// <returns>
481
        /// <list type="bullet">
482
        /// <item>
483
        /// <term><c>true</c></term>
484
        /// <description>
485
        /// When <paramref name="ver1"/> is less than <paramref name="ver2"/>.
486
        /// </description>
487
        /// </item>
488
        /// <item>
489
        /// <term><c>false</c></term>
490
        /// <description>
491
        /// When <paramref name="ver1"/> is not less than <paramref name="ver2"/>.
492
        /// </description>
493
        /// </item>
494
        /// </list>
495
        /// </returns>
496
        public static bool operator <(ModuleVersion ver1, ModuleVersion ver2)
497
            => ver1.CompareTo(ver2) < 0;
2✔
498

499
        /// <summary>
500
        /// Compares two <see cref="ModuleVersion"/> objects to determine if the first is less than or equal to the
501
        /// second.
502
        /// </summary>
503
        /// <param name="ver1">The first of two <see cref="ModuleVersion"/> objects to compare.</param>
504
        /// <param name="ver2">The second of two <see cref="ModuleVersion"/> objects to compare.</param>
505
        /// <returns>
506
        /// <list type="bullet">
507
        /// <item>
508
        /// <term><c>true</c></term>
509
        /// <description>
510
        /// When <paramref name="ver1"/> is less than or equal to <paramref name="ver2"/>.
511
        /// </description>
512
        /// </item>
513
        /// <item>
514
        /// <term><c>false</c></term>
515
        /// <description>
516
        /// When <paramref name="ver1"/> is not less than nor equal to <paramref name="ver2"/>.
517
        /// </description>
518
        /// </item>
519
        /// </list>
520
        /// </returns>
521
        public static bool operator <=(ModuleVersion ver1, ModuleVersion ver2)
522
            => ver1.CompareTo(ver2) <= 0;
2✔
523

524
        /// <summary>
525
        /// Compares two <see cref="ModuleVersion"/> objects to determine if the first is greater than the second.
526
        /// </summary>
527
        /// <param name="ver1">The first of two <see cref="ModuleVersion"/> objects to compare.</param>
528
        /// <param name="ver2">The second of two <see cref="ModuleVersion"/> objects to compare.</param>
529
        /// <returns>
530
        /// <list type="bullet">
531
        /// <item>
532
        /// <term><c>true</c></term>
533
        /// <description>
534
        /// When <paramref name="ver1"/> is greater than <paramref name="ver2"/>.
535
        /// </description>
536
        /// </item>
537
        /// <item>
538
        /// <term><c>false</c></term>
539
        /// <description>
540
        /// When <paramref name="ver1"/> is not greater than <paramref name="ver2"/>.
541
        /// </description>
542
        /// </item>
543
        /// </list>
544
        /// </returns>
545
        public static bool operator >(ModuleVersion ver1, ModuleVersion ver2)
546
            => ver1.CompareTo(ver2) > 0;
2✔
547

548
        /// <summary>
549
        /// Compares two <see cref="ModuleVersion"/> objects to determine if the first is less than or equal to the
550
        /// second.
551
        /// </summary>
552
        /// <param name="ver1">The first of two <see cref="ModuleVersion"/> objects to compare.</param>
553
        /// <param name="ver2">The second of two <see cref="ModuleVersion"/> objects to compare.</param>
554
        /// <returns>
555
        /// <list type="bullet">
556
        /// <item>
557
        /// <term><c>true</c></term>
558
        /// <description>
559
        /// When <paramref name="ver1"/> is greater than or equal to <paramref name="ver2"/>.
560
        /// </description>
561
        /// </item>
562
        /// <item>
563
        /// <term><c>false</c></term>
564
        /// <description>
565
        /// When <paramref name="ver1"/> is not greater than nor equal to <paramref name="ver2"/>.
566
        /// </description>
567
        /// </item>
568
        /// </list>
569
        /// </returns>
570
        public static bool operator >=(ModuleVersion ver1, ModuleVersion ver2)
571
            => ver1.CompareTo(ver2) >= 0;
2✔
572

573
        private struct Comparison
574
        {
575
            public int    CompareTo;
576
            public string FirstRemainder;
577
            public string SecondRemainder;
578
        }
579
    }
580
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc