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

KSP-CKAN / CKAN / 25198663652

01 May 2026 01:58AM UTC coverage: 87.472% (+1.6%) from 85.851%
25198663652

push

github

HebaruSan
Merge #4594 Windows dark mode in .NET 10 build

1982 of 2112 branches covered (93.84%)

Branch coverage included in aggregate %.

35 of 36 new or added lines in 7 files covered. (97.22%)

33 existing lines in 24 files now uncovered.

8491 of 9861 relevant lines covered (86.11%)

2.69 hits per line

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

84.55
/Core/Configuration/JsonConfiguration.cs
1
using System;
2
using System.ComponentModel;
3
using System.Collections.Generic;
4
using System.IO;
5
using System.Linq;
6
using System.Diagnostics.CodeAnalysis;
7
using System.Runtime.CompilerServices;
8

9
using Newtonsoft.Json;
10

11
using CKAN.IO;
12
using CKAN.Extensions;
13
using CKAN.Games;
14
using CKAN.Games.KerbalSpaceProgram;
15

16
namespace CKAN.Configuration
17
{
18
    public class JsonConfiguration : IConfiguration
19
    {
20
        #region JSON structures
21

22
        [JsonObject(MemberSerialization   = MemberSerialization.OptOut,
23
                    ItemNullValueHandling = NullValueHandling.Ignore)]
24
        [JsonConverter(typeof(ConfigConverter))]
25
        private class Config
26
        {
27
            public string?                       AutoStartInstance    { get; set; }
28
            public string?                       DownloadCacheDir     { get; set; }
29
            public long?                         CacheSizeLimit       { get; set; }
30
            public int?                          RefreshRate          { get; set; }
31
            public string?                       Language             { get; set; }
32
            public IList<GameInstanceEntry>?     GameInstances        { get; set; } = new List<GameInstanceEntry>();
3✔
33
            public IDictionary<string, string>?  AuthTokens           { get; set; } = new Dictionary<string, string>();
3✔
34
            [JsonProperty("GlobalInstallFiltersByGame")]
35
            [JsonConverter(typeof(JsonToGamesDictionaryConverter))]
36
            public Dictionary<string, string[]>? GlobalInstallFilters { get; set; } = new Dictionary<string, string[]>();
3✔
37
            public string?[]?                    PreferredHosts       { get; set; } = Array.Empty<string>();
3✔
38
            public bool?                         DevBuilds            { get; set; }
39
        }
40

41
        /// <summary>
42
        /// Protect old clients from trying to load a file they can't parse
43
        /// </summary>
44
        private class ConfigConverter : JsonPropertyNamesChangedConverter
45
        {
46
            protected override Dictionary<string, string> mapping
47
                => new Dictionary<string, string>
3✔
48
                {
49
                    { "KspInstances",         "GameInstances" },
50
                    { "GlobalInstallFilters", "GlobalInstallFiltersByGame" },
51
                };
52
        }
53

54
        private class GameInstanceEntry
55
        {
56
            [JsonConstructor]
57
            public GameInstanceEntry(string name, string path, string game)
3✔
58
            {
59
                Name = name;
3✔
60
                Path = path;
3✔
61
                Game = game;
3✔
62
            }
3✔
63

64
            public string Name { get; set; }
65
            public string Path { get; set; }
66
            public string Game { get; set; }
67
        }
68

69
        #endregion
70

71
        /// <summary>
72
        /// Loads configuration from the given file, or the default path if null.
73
        /// </summary>
74
        public JsonConfiguration(string? newConfig = null)
3✔
75
        {
76
            configFile = newConfig ?? defaultConfigFile;
3✔
77
            LoadConfig();
3✔
78
        }
3✔
79

80
        public event PropertyChangedEventHandler? PropertyChanged;
81

82
        // The standard location of the config file. Where this actually points is platform dependent,
83
        // but it's the same place as the downloads folder. The location can be overwritten with the
84
        // CKAN_CONFIG_FILE environment variable.
85
        public static readonly string defaultConfigFile =
3✔
86
            Environment.GetEnvironmentVariable("CKAN_CONFIG_FILE")
87
            ?? Path.Combine(CKANPathUtils.AppDataPath, "config.json");
88

89
        // The actual config file state and its location on the disk (we allow
90
        // the location to be changed for unit tests). This version is considered
91
        // authoritative, and we save it to the disk every time it gets changed.
92
        //
93
        // If you have multiple instances of CKAN running at the same time, each will
94
        // believe that their copy of the config file in memory is authoritative, so
95
        // changes made by one copy will not be respected by the other.
96
        private readonly string configFile = defaultConfigFile;
3✔
97
        private Config config;
98

99
        public string? DownloadCacheDir
100
        {
101
            get => config.DownloadCacheDir;
3✔
102

103
            set
104
            {
105
                if (string.IsNullOrEmpty(value))
3✔
106
                {
107
                    config.DownloadCacheDir = null;
3✔
108
                }
109
                else
110
                {
111
                    if (!Path.IsPathRooted(value))
3✔
112
                    {
113
                        value = Path.GetFullPath(value);
3✔
114
                    }
115
                    config.DownloadCacheDir = value;
3✔
116
                }
117
                SaveConfig();
3✔
118
            }
3✔
119
        }
120

121
        public long? CacheSizeLimit
122
        {
123
            get => config.CacheSizeLimit;
3✔
124

125
            set
126
            {
127
                config.CacheSizeLimit = value < 0 ? null : value;
3✔
128
                SaveConfig();
3✔
129
            }
3✔
130
        }
131

132
        public int RefreshRate
133
        {
134
            get => config.RefreshRate ?? 0;
3✔
135

136
            set
137
            {
138
                config.RefreshRate = value <= 0 ? null : value;
3✔
139
                SaveConfig();
3✔
140
            }
3✔
141
        }
142

143
        public string? Language
144
        {
145
            get => config.Language;
3✔
146

147
            set
148
            {
149
                if (Utilities.AvailableLanguages.Contains(value))
3✔
150
                {
151
                    config.Language = value;
3✔
152
                    SaveConfig();
3✔
153
                }
154
            }
3✔
155
        }
156

157
        public string? AutoStartInstance
158
        {
159
            get => config.AutoStartInstance;
3✔
160

161
            set
162
            {
163
                config.AutoStartInstance = value;
3✔
164
                SaveConfig();
3✔
165
            }
3✔
166
        }
167

168
        public IEnumerable<Tuple<string, string, string>> GetInstances()
169
            => config.GameInstances?.Select(instance =>
3✔
170
                    new Tuple<string, string, string>(
3✔
171
                        instance.Name,
172
                        instance.Path,
173
                        instance.Game))
174
                ?? Enumerable.Empty<Tuple<string, string, string>>();
175

176
        public void SetRegistryToInstances(SortedList<string, GameInstance> instances)
177
        {
178
            config.GameInstances = instances.Select(inst => new GameInstanceEntry(inst.Key,
3✔
179
                                                                                  inst.Value.GameDir,
180
                                                                                  inst.Value.Game.ShortName))
181
                                            .ToList();
182
            SaveConfig();
3✔
183
        }
3✔
184

185
        public IEnumerable<string> GetAuthTokenHosts()
186
            => config.AuthTokens?.Keys
3✔
187
                                ?? Enumerable.Empty<string>();
188

189
        public bool TryGetAuthToken(string host,
190
                                    [NotNullWhen(true)] out string? token)
191
        {
192
            if (config.AuthTokens == null)
3✔
193
            {
194
                token = null;
×
195
                return false;
×
196
            }
197
            return config.AuthTokens.TryGetValue(host, out token);
3✔
198
        }
199

200
        public void SetAuthToken(string host, string? token)
201
        {
202
            if (token is {Length: > 0})
3✔
203
            {
204
                config.AuthTokens ??= new Dictionary<string, string>();
3✔
205
                config.AuthTokens[host] = token;
3✔
206
            }
207
            else if (config.AuthTokens != null
×
208
                     && config.AuthTokens.ContainsKey(host))
209
            {
210
                if (config.AuthTokens.Count > 1)
×
211
                {
212
                    config.AuthTokens.Remove(host);
×
213
                }
214
                else
215
                {
216
                    config.AuthTokens = null;
×
217
                }
218
            }
219
            else
220
            {
221
                // No changes needed, skip saving
222
                return;
×
223
            }
224
            SaveConfig();
3✔
225
        }
3✔
226

227
        public string[] GetGlobalInstallFilters(IGame game)
228
            => config.GlobalInstallFilters?.GetValueOrDefault(game.ShortName)
3✔
229
                                          ?? Array.Empty<string>();
230

231
        public void SetGlobalInstallFilters(IGame game, string[] value)
232
        {
233
            if (value.Length > 0)
3✔
234
            {
235
                // Set the list for this game
236
                config.GlobalInstallFilters ??= new Dictionary<string, string[]>();
3✔
237
                config.GlobalInstallFilters[game.ShortName] = value;
3✔
238
            }
239
            else if (config.GlobalInstallFilters != null
×
240
                     && config.GlobalInstallFilters.ContainsKey(game.ShortName))
241
            {
242
                if (config.GlobalInstallFilters.Count > 1)
×
243
                {
244
                    // Purge this game's entry
245
                    config.GlobalInstallFilters.Remove(game.ShortName);
×
246
                }
247
                else
248
                {
249
                    // Discard empty dictionary
250
                    config.GlobalInstallFilters = null;
×
251
                }
252
            }
253
            else
254
            {
255
                // No changes needed, skip saving and notifications
256
                return;
×
257
            }
258
            SaveConfig();
3✔
259
            // Refresh the Contents tab
260
            OnPropertyChanged();
3✔
261
        }
3✔
262

263
        public string?[] PreferredHosts
264
        {
265
            get => config.PreferredHosts ?? Array.Empty<string>();
3✔
266

267
            set
268
            {
269
                config.PreferredHosts = value is {Length: > 0} ? value : null;
3✔
270
                SaveConfig();
3✔
271
            }
3✔
272
        }
273

274
        public bool? DevBuilds
275
        {
276
            get => config.DevBuilds;
3✔
277

278
            set
279
            {
280
                config.DevBuilds = value;
3✔
281
                SaveConfig();
3✔
282
            }
3✔
283
        }
284

285
        private void OnPropertyChanged([CallerMemberName] string? name = null)
286
        {
287
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
3✔
UNCOV
288
        }
×
289

290
        // <summary>
291
        // Save the JSON configuration file.
292
        // </summary>
293
        private void SaveConfig()
294
        {
295
            JsonConvert.SerializeObject(config, Formatting.Indented)
3✔
296
                       .WriteThroughTo(configFile);
297
        }
3✔
298

299
        /// <summary>
300
        /// Load the JSON configuration file.
301
        ///
302
        /// If the configuration file does not exist, this will create it and then
303
        /// try to populate it with values in the registry left from the old system.
304
        /// </summary>
305
        [MemberNotNull(nameof(config))]
306
        private void LoadConfig()
307
        {
308
            try
309
            {
310
                config = JsonConvert.DeserializeObject<Config>(File.ReadAllText(configFile))
3✔
311
                         ?? new Config();
312

313
                if (config.GameInstances == null)
3✔
314
                {
315
                    config.GameInstances = new List<GameInstanceEntry>();
×
316
                }
317
                else
318
                {
319
                    var gameName = new KerbalSpaceProgram().ShortName;
3✔
320
                    foreach (var e in config.GameInstances)
9✔
321
                    {
322
                        e.Game ??= gameName;
3✔
323
                    }
324
                }
325
            }
3✔
326
            catch (Exception ex) when (ex is FileNotFoundException or DirectoryNotFoundException)
3✔
327
            {
328
                // This runs if the configuration does not exist
329
                // Ensure the directory exists
330
                new FileInfo(configFile).Directory?.Create();
3✔
331

332
                // Create a new configuration
333
                config = new Config();
3✔
334

335
                SaveConfig();
3✔
336
            }
3✔
337
            if (
3✔
338
                #if NET6_0_OR_GREATER
339
                Platform.IsWindows &&
340
                #endif
341
                Win32RegistryConfiguration.DoesRegistryConfigurationExist())
342
            {
343
                // Clean up very old Windows registry keys
344
                Win32RegistryConfiguration.DeleteAllKeys();
×
345
            }
346
        }
3✔
347
    }
348
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc