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

KSP-CKAN / CKAN / 30355498790

28 Jul 2026 11:36AM UTC coverage: 88.009% (+0.1%) from 87.868%
30355498790

Pull #4694

github

web-flow
Merge 35045c6c4 into 5e956e23c
Pull Request #4694: New URL protocol and handler improvements

2054 of 2181 branches covered (94.18%)

Branch coverage included in aggregate %.

159 of 165 new or added lines in 7 files covered. (96.36%)

8794 of 10145 relevant lines covered (86.68%)

1.81 hits per line

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

88.68
/Core/IO/ProtocolRouter.cs
1
using System;
2
using System.Collections.Generic;
3
using System.Linq;
4
using System.Web;
5

6
namespace CKAN.IO
7
{
8
    public enum UrlError
9
    {
10
        BadSyntax,
11
        UnknownOperation,
12
        NoValidKeys,
13
    }
14

15
    public static class ProtocolRouter
16
    {
17
        public static event Action<string>? OnFocus;
18
        public static event Action<string>? OnSearch;
19
        public static event Action<List<(string Mod, string? Version)>>? OnInstall;
20
        public static event Action<UrlError>? OnError;
21

22
        // --url value set by cmdline.
23
        public static string? PendingLaunchUrl { get; set; }
24

25
        public static string ErrorMessage(UrlError error)
NEW
26
            => error switch
×
27
               {
NEW
28
                   UrlError.BadSyntax => Properties.Resources.UrlBadSyntax,
×
NEW
29
                   UrlError.UnknownOperation => Properties.Resources.UrlUnknownOperation,
×
NEW
30
                   UrlError.NoValidKeys => Properties.Resources.UrlNoValidKeys,
×
NEW
31
                   _ => Properties.Resources.UrlBadSyntax,
×
32
               };
33

34
        // Call once subscribed. Handles the --url URL if there was one.
35
        public static void HandlePendingLaunchUrl()
36
        {
37
            Handle(PendingLaunchUrl);
2✔
38
            PendingLaunchUrl = null;
2✔
39
        }
2✔
40

41
        public static void Handle(string? rawUrl)
42
        {
43
            if (rawUrl == null || string.IsNullOrWhiteSpace(rawUrl))
2✔
44
            {
45
                return;
2✔
46
            }
47

48
            // The --url option works without a scheme, e.g. install?mod=JNSQ.
49
            if (!rawUrl.StartsWith("ckan://", StringComparison.OrdinalIgnoreCase))
2✔
50
            {
51
                rawUrl = "ckan://" + rawUrl;
2✔
52
            }
53

54
            // Will fail if rawUrl isn't in the form `scheme://host?key=value&key=value`
55
            if (!Uri.TryCreate(rawUrl, UriKind.Absolute, out var uri))
2✔
56
            {
57
                OnError?.Invoke(UrlError.BadSyntax);
2✔
58
                return;
2✔
59
            }
60

61
            var query = HttpUtility.ParseQueryString(uri.Query);
2✔
62

63
            // Uri Host is always lower case.
64
            switch (uri.Host)
2✔
65
            {
66
                case "focus":
67
                    RouteValue(query.GetValues("mod"), OnFocus);
2✔
68
                    break;
2✔
69
                case "search":
70
                    RouteValue(query.GetValues("q"), OnSearch);
2✔
71
                    break;
2✔
72
                case "install":
73
                    RouteMods(query.GetValues("mod"), OnInstall);
2✔
74
                    break;
2✔
75
                default:
76
                    OnError?.Invoke(UrlError.UnknownOperation);
2✔
77
                    break;
78
            }
79
        }
2✔
80

81
        private static void RouteValue(string[]? values, Action<string>? handler)
82
        {
83
            // A URL could repeat a key. We only want the first value.
84
            var value = values?.FirstOrDefault(v => !string.IsNullOrWhiteSpace(v));
2✔
85

86
            if (value == null)
2✔
87
            {
88
                OnError?.Invoke(UrlError.NoValidKeys);
2✔
89
                return;
2✔
90
            }
91

92
            handler?.Invoke(value.Trim());
2✔
93
        }
2✔
94

95
        private static void RouteMods(string[]? modValues, Action<List<(string Mod, string? Version)>>? handler)
96
        {
97
            modValues = modValues?.Where(x => !string.IsNullOrWhiteSpace(x)).ToArray();
2✔
98

99
            if (modValues == null || modValues.Length == 0)
2✔
100
            {
101
                OnError?.Invoke(UrlError.NoValidKeys);
2✔
102
                return;
2✔
103
            }
104

105
            handler?.Invoke(modValues.Select(SplitPinned).ToList());
2✔
106
        }
2✔
107

108
        // Split "mod:version" or "mod" into "mod" and a nullable "version".
109
        private static (string Mod, string? Version) SplitPinned(string mod)
110
        {
111
            int colon = mod.IndexOf(':');
2✔
112

113
            return colon == -1 ? (mod, null) : (mod[..colon], mod[(colon + 1)..]);
2✔
114
        }
115
    }
116
}
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