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

KSP-CKAN / CKAN / 30176072171

25 Jul 2026 09:39PM UTC coverage: 88.063% (+0.2%) from 87.868%
30176072171

Pull #4694

github

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

2057 of 2182 branches covered (94.27%)

Branch coverage included in aggregate %.

158 of 169 new or added lines in 7 files covered. (93.49%)

26 existing lines in 2 files now uncovered.

8802 of 10149 relevant lines covered (86.73%)

1.81 hits per line

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

87.72
/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. Only read by HandlePendingLaunchUrl.
23
        public static string? PendingLaunchUrl { internal get; set; }
24

NEW
25
        public static bool HasPendingLaunchUrl => PendingLaunchUrl != null;
×
26

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

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

43
        public static void Handle(string? rawUrl)
44
        {
45
            // Double null check to appease compiler.
46
            if (rawUrl == null || string.IsNullOrWhiteSpace(rawUrl))
2✔
47
            {
48
                return;
2✔
49
            }
50

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

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

64
            var query = HttpUtility.ParseQueryString(uri.Query);
2✔
65

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

84
        private static void RouteValue(string? value, Action<string>? handler)
85
        {
86
            // We expect one value at this point, but the URL could include multiple, and they will end up here as CSV.
87

88
            if (value != null)
2✔
89
            {
90
                int comma = value.IndexOf(',');
2✔
91
                string first = (comma >= 0 ? value[..comma] : value).Trim();
2✔
92

93
                if (first.Length > 0)
2✔
94
                {
95
                    handler?.Invoke(first);
2✔
96
                    return;
2✔
97
                }
98
            }
99

100
            OnError?.Invoke(UrlError.NoValidKeys);
2✔
101
        }
2✔
102

103
        private static void RouteMods(string[]? modValues, Action<List<(string Mod, string? Version)>>? handler)
104
        {
105
            modValues = modValues?.Where(x => !string.IsNullOrWhiteSpace(x)).ToArray();
2✔
106

107
            if (modValues == null || modValues.Length == 0)
2✔
108
            {
109
                OnError?.Invoke(UrlError.NoValidKeys);
2✔
110
                return;
2✔
111
            }
112

113
            handler?.Invoke(modValues.Select(SplitPinned).ToList());
2✔
114
        }
2✔
115

116
        // Split "mod:version" or "mod" into "mod" and a nullable "version".
117
        private static (string Mod, string? Version) SplitPinned(string mod)
118
        {
119
            int colon = mod.IndexOf(':');
2✔
120

121
            return colon == -1 ? (mod, null) : (mod[..colon], mod[(colon + 1)..]);
2✔
122
        }
123
    }
124
}
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