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

KSP-CKAN / CKAN / 15432034930

04 Jun 2025 02:19AM UTC coverage: 30.322% (+0.04%) from 30.28%
15432034930

Pull #4386

github

web-flow
Merge f8b59bcd0 into 4cf303cc8
Pull Request #4386: Mod list multi-select

4063 of 14340 branches covered (28.33%)

Branch coverage included in aggregate %.

55 of 170 new or added lines in 12 files covered. (32.35%)

59 existing lines in 5 files now uncovered.

13712 of 44281 relevant lines covered (30.97%)

0.63 hits per line

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

57.39
/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
#if NET5_0_OR_GREATER
7
using System.Runtime.Versioning;
8
#endif
9
using System.Diagnostics.CodeAnalysis;
10
using System.Runtime.CompilerServices;
11

12
using Newtonsoft.Json;
13

14
using CKAN.IO;
15
using CKAN.Extensions;
16
using CKAN.Games;
17
using CKAN.Games.KerbalSpaceProgram;
18

19
namespace CKAN.Configuration
20
{
21
    public class JsonConfiguration : IConfiguration
22
    {
23
        #region JSON structures
24

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

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

57
        private class GameInstanceEntry
58
        {
59
            [JsonConstructor]
60
            public GameInstanceEntry(string name, string path, string game)
2✔
61
            {
2✔
62
                Name = name;
2✔
63
                Path = path;
2✔
64
                Game = game;
2✔
65
            }
2✔
66

67
            public string Name { get; set; }
68
            public string Path { get; set; }
69
            public string Game { get; set; }
70
        }
71

72
        #endregion
73

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

83
        public event PropertyChangedEventHandler? PropertyChanged;
84

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

92
        public static readonly string DefaultDownloadCacheDir =
2✔
93
            Path.Combine(CKANPathUtils.AppDataPath, "downloads");
94

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

105
        public string? DownloadCacheDir
106
        {
107
            get => config.DownloadCacheDir ?? DefaultDownloadCacheDir;
2✔
108

109
            set
110
            {
2✔
111
                if (string.IsNullOrEmpty(value))
2✔
112
                {
2✔
113
                    config.DownloadCacheDir = null;
2✔
114
                }
2✔
115
                else
116
                {
2✔
117
                    if (!Path.IsPathRooted(value))
2✔
118
                    {
2✔
119
                        value = Path.GetFullPath(value);
2✔
120
                    }
2✔
121
                    config.DownloadCacheDir = value;
2✔
122
                }
2✔
123
                SaveConfig();
2✔
124
            }
2✔
125
        }
126

127
        public long? CacheSizeLimit
128
        {
129
            get => config.CacheSizeLimit;
2✔
130

131
            set
132
            {
2✔
133
                config.CacheSizeLimit = value < 0 ? null : value;
2✔
134
                SaveConfig();
2✔
135
            }
2✔
136
        }
137

138
        public int RefreshRate
139
        {
140
            get => config.RefreshRate ?? 0;
2✔
141

142
            set
143
            {
2✔
144
                config.RefreshRate = value <= 0 ? null : value;
2✔
145
                SaveConfig();
2✔
146
            }
2✔
147
        }
148

149
        public string? Language
150
        {
151
            get => config.Language;
×
152

153
            set
154
            {
×
155
                if (Utilities.AvailableLanguages.Contains(value))
×
156
                {
×
157
                    config.Language = value;
×
158
                    SaveConfig();
×
159
                }
×
160
            }
×
161
        }
162

163
        public string? AutoStartInstance
164
        {
165
            get => config.AutoStartInstance ?? "";
2✔
166

167
            set
168
            {
2✔
169
                config.AutoStartInstance = value ?? "";
2✔
170
                SaveConfig();
2✔
171
            }
2✔
172
        }
173

174
        public IEnumerable<Tuple<string, string, string>> GetInstances()
175
            => config.GameInstances?.Select(instance =>
2!
176
                    new Tuple<string, string, string>(
2✔
177
                        instance.Name,
178
                        instance.Path,
179
                        instance.Game))
180
                ?? Enumerable.Empty<Tuple<string, string, string>>();
181

182
        public void SetRegistryToInstances(SortedList<string, GameInstance> instances)
183
        {
2✔
184
            config.GameInstances = instances.Select(inst => new GameInstanceEntry(inst.Key,
2✔
185
                                                                                  inst.Value.GameDir(),
186
                                                                                  inst.Value.game.ShortName))
187
                                            .ToList();
188
            SaveConfig();
2✔
189
        }
2✔
190

191
        public IEnumerable<string> GetAuthTokenHosts()
192
            => config.AuthTokens?.Keys
2!
193
                                ?? Enumerable.Empty<string>();
194

195
        public bool TryGetAuthToken(string host,
196
                                    [NotNullWhen(returnValue: true)] out string? token)
197
        {
2✔
198
            if (config.AuthTokens == null)
2!
199
            {
×
200
                token = null;
×
201
                return false;
×
202
            }
203
            return config.AuthTokens.TryGetValue(host, out token);
2✔
204
        }
2✔
205

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

233
        public string[] GetGlobalInstallFilters(IGame game)
234
            => config.GlobalInstallFilters?.GetOrDefault(game.ShortName)
2!
235
                                          ?? Array.Empty<string>();
236

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

269
        public string?[] PreferredHosts
270
        {
271
            get => config.PreferredHosts ?? Array.Empty<string>();
2!
272

273
            set
274
            {
×
275
                config.PreferredHosts = value is {Length: > 0} ? value : null;
×
276
                SaveConfig();
×
277
            }
×
278
        }
279

280
        public bool? DevBuilds
281
        {
282
            get => config.DevBuilds;
×
283

284
            set
285
            {
×
286
                config.DevBuilds = value;
×
287
                SaveConfig();
×
288
            }
×
289
        }
290

291
        private void OnPropertyChanged([CallerMemberName] string? name = null)
292
        {
×
293
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
×
294
        }
×
295

296
        // <summary>
297
        // Save the JSON configuration file.
298
        // </summary>
299
        private void SaveConfig()
300
        {
2✔
301
            File.WriteAllText(configFile,
2✔
302
                              JsonConvert.SerializeObject(config, Formatting.Indented));
303
        }
2✔
304

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

319
                if (config.GameInstances == null)
2!
320
                {
×
321
                    config.GameInstances = new List<GameInstanceEntry>();
×
322
                }
×
323
                else
324
                {
2✔
325
                    var gameName = new KerbalSpaceProgram().ShortName;
2✔
326
                    foreach (var e in config.GameInstances)
5✔
327
                    {
2✔
328
                        e.Game ??= gameName;
2!
329
                    }
2✔
330
                }
2✔
331
            }
2!
332
            catch (Exception ex) when (ex is FileNotFoundException or DirectoryNotFoundException)
2✔
333
            {
2✔
334
                // This runs if the configuration does not exist
335
                // Ensure the directory exists
336
                new FileInfo(configFile).Directory?.Create();
2!
337

338
                // Create a new configuration
339
                config = new Config();
2✔
340

341
                SaveConfig();
2✔
342
            }
2✔
343
            if (
2!
344
                #if NET6_0_OR_GREATER
345
                Platform.IsWindows &&
346
                #endif
347
                Win32RegistryConfiguration.DoesRegistryConfigurationExist())
UNCOV
348
            {
×
349
                // Clean up very old Windows registry keys
NEW
350
                Win32RegistryConfiguration.DeleteAllKeys();
×
NEW
351
            }
×
352
        }
2✔
353
    }
354
}
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