• 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

90.09
/Core/Repositories/RepositoryData.cs
1
using System;
2
using System.IO;
3
using System.Text;
4
using System.Linq;
5
using System.Collections.Generic;
6
using System.Collections.Concurrent;
7
using System.Runtime.Serialization;
8

9
using Newtonsoft.Json;
10
using Newtonsoft.Json.Linq;
11
using ChinhDo.Transactions;
12
using ICSharpCode.SharpZipLib.GZip;
13
using ICSharpCode.SharpZipLib.Tar;
14
using ICSharpCode.SharpZipLib.Zip;
15
using log4net;
16

17
using CKAN.IO;
18
using CKAN.Versioning;
19
using CKAN.Games;
20

21
namespace CKAN
22
{
23
    using ArchiveEntry = Tuple<CkanModule?,
24
                               SortedDictionary<string, int>?,
25
                               GameVersion[]?,
26
                               Repository[]?,
27
                               long>;
28

29
    using ArchiveList = Tuple<List<CkanModule>,
30
                              SortedDictionary<string, int>?,
31
                              GameVersion[]?,
32
                              Repository[]?,
33
                              bool>;
34

35
    /// <summary>
36
    /// Represents everything we retrieve from one metadata repository
37
    /// </summary>
38
    public class RepositoryData
39
    {
40
        /// <summary>
41
        /// The available modules from this repository
42
        /// </summary>
43
        [JsonProperty("available_modules", NullValueHandling = NullValueHandling.Ignore)]
44
        [JsonConverter(typeof(JsonParallelDictionaryConverter<AvailableModule>))]
45
        public readonly Dictionary<string, AvailableModule>? AvailableModules;
46

47
        /// <summary>
48
        /// The download counts from this repository's download_counts.json
49
        /// </summary>
50
        [JsonProperty("download_counts", NullValueHandling = NullValueHandling.Ignore)]
51
        public readonly SortedDictionary<string, int>? DownloadCounts;
52

53
        /// <summary>
54
        /// The game versions from this repository's builds.json
55
        /// Currently not used, maybe in the future
56
        /// </summary>
57
        [JsonProperty("known_game_versions", NullValueHandling = NullValueHandling.Ignore)]
58
        public readonly GameVersion[]? KnownGameVersions;
59

60
        /// <summary>
61
        /// The other repositories listed in this repo's repositories.json
62
        /// Currently not used, maybe in the future
63
        /// </summary>
64
        [JsonProperty("repositories", NullValueHandling = NullValueHandling.Ignore)]
65
        public readonly Repository[]? Repositories;
66

67
        /// <summary>
68
        /// true if any module we found requires a newer client version, false otherwise
69
        /// </summary>
70
        [JsonIgnore]
71
        public readonly bool UnsupportedSpec;
72

73
        private RepositoryData(Dictionary<string, AvailableModule> modules,
3✔
74
                               SortedDictionary<string, int>       counts,
75
                               GameVersion[]                       versions,
76
                               Repository[]                        repos)
77
        {
78
            AvailableModules  = modules;
3✔
79
            DownloadCounts    = counts;
3✔
80
            KnownGameVersions = versions;
3✔
81
            Repositories      = repos;
3✔
82
        }
3✔
83

84
        /// <summary>
85
        /// Instantiate a repo data object
86
        /// </summary>
87
        /// <param name="modules">The available modules contained in this repo</param>
88
        /// <param name="counts">Download counts from this repo</param>
89
        /// <param name="versions">Game versions in this repo</param>
90
        /// <param name="repos">Contents of repositories.json in this repo</param>
91
        /// <param name="unsupportedSpec">true if any module we found requires a newer client version, false otherwise</param>
92
        public RepositoryData(IEnumerable<CkanModule>?       modules,
93
                              SortedDictionary<string, int>? counts,
94
                              IEnumerable<GameVersion>?      versions,
95
                              IEnumerable<Repository>?       repos,
96
                              bool                           unsupportedSpec)
97
            : this((modules ?? Enumerable.Empty<CkanModule>())
3✔
98
                           .GroupBy(m => m.identifier)
3✔
99
                           .ToDictionary(grp => grp.Key,
3✔
100
                                         grp => new AvailableModule(grp.Key, grp)),
3✔
101
                   counts ?? new SortedDictionary<string, int>(),
102
                   (versions ?? Enumerable.Empty<GameVersion>()).ToArray(),
103
                   (repos ?? Enumerable.Empty<Repository>()).ToArray())
104
        {
105
            UnsupportedSpec   = unsupportedSpec;
3✔
106
        }
3✔
107

108
        [JsonConstructor]
109
        private RepositoryData()
3✔
110
        {
111
        }
3✔
112

113
        /// <summary>
114
        /// Save this repo data object to a JSON file
115
        /// </summary>
116
        /// <param name="path">Filename of the JSON file to create or overwrite</param>
117
        public void SaveTo(string path)
118
        {
119
            StringWriter sw = new StringWriter(new StringBuilder());
3✔
120
            using (JsonTextWriter writer = new JsonTextWriter(sw)
3✔
121
            {
122
                Formatting  = Formatting.Indented,
123
                Indentation = 0,
124
            })
125
            {
126
                JsonSerializer serializer = new JsonSerializer();
3✔
127
                serializer.Serialize(writer, this);
3✔
128
            }
3✔
129
            var txFileMgr = new TxFileManager();
3✔
130
            txFileMgr.WriteAllText(path, sw + Environment.NewLine,
3✔
131
                                   Encoding.UTF8);
132
        }
3✔
133

134
        /// <summary>
135
        /// Load a previously cached repo data object from JSON on disk
136
        /// </summary>
137
        /// <param name="path">Filename of the JSON file to load</param>
138
        /// <param name="progress">Progress notifier to receive updates of percent completion of this file</param>
139
        /// <returns>A repo data object or null if there is no such file</returns>
140
        public static RepositoryData? FromJson(string path, IProgress<int>? progress)
141
        {
142
            try
143
            {
144
                long fileSize = new FileInfo(path).Length;
3✔
145
                log.DebugFormat("Trying to load repository data from {0}", path);
3✔
146
                // Ain't OOP grand?!
147
                using (var stream = File.OpenRead(path))
3✔
148
                using (var progressStream = new ReadProgressStream(
3✔
149
                    stream,
150
                    progress == null
151
                        ? null
152
                        // Treat JSON parsing as the first 50%
153
                        : new ProgressImmediate<long>(p => progress.Report((int)(50 * p / fileSize)))))
3✔
154
                using (var reader = new StreamReader(progressStream, Encoding.UTF8))
3✔
155
                using (var jStream = new JsonTextReader(reader))
3✔
156
                {
157
                    var settings = new JsonSerializerSettings()
3✔
158
                    {
159
                        DateTimeZoneHandling = DateTimeZoneHandling.Utc,
160
                        Context = new StreamingContext(
161
                            StreamingContextStates.Other,
162
                            progress == null
163
                                ? null
164
                                : new ProgressImmediate<int>(p =>
165
                                    // Treat CkanModule creation as the last 50%
166
                                    progress.Report(50 + (p / 2)))),
3✔
167
                    };
168
                    return JsonSerializer.Create(settings)
3✔
169
                                         .Deserialize<RepositoryData>(jStream);
170
                }
171
            }
172
            catch (FileNotFoundException exc)
3✔
173
            {
174
                log.DebugFormat("Valid repository data not found at {0}: {1}",
3✔
175
                                path, exc.Message);
176
                return null;
3✔
177
            }
178
        }
3✔
179

180
        public static RepositoryData FromStream(Stream stream, IGame game, IProgress<long> progress)
181
        {
182
            switch (FileIdentifier.IdentifyFile(stream))
3✔
183
            {
184
                case FileType.TarGz: return FromTarGzStream(stream, game, progress);
3✔
185
                case FileType.Zip:   return FromZipStream(stream, game, progress);
3✔
186
                default: throw new UnsupportedKraken(Properties.Resources.NetRepoNotATarGzStream);
×
187
            }
188
        }
189

190
        private static RepositoryData FromTarGzStream(Stream inputStream, IGame game, IProgress<long> progress)
191
        {
192
            inputStream.Seek(0, SeekOrigin.Begin);
3✔
193
            using (var progressStream = new ReadProgressStream(inputStream, progress))
3✔
194
            using (var gzipStream     = new GZipInputStream(progressStream))
3✔
195
            using (var tarStream      = new TarInputStream(gzipStream, Encoding.UTF8))
3✔
196
            {
197
                (List<CkanModule>               modules,
3✔
198
                 SortedDictionary<string, int>? counts,
199
                 GameVersion[]?                 versions,
200
                 Repository[]?                  repos,
201
                 bool                           unsupSpec) = AggregateArchiveEntries(archiveEntriesFromTar(tarStream, game));
202
                return new RepositoryData(modules, counts, versions, repos, unsupSpec);
3✔
203
            }
204
        }
3✔
205

206
        private static ParallelQuery<ArchiveEntry?> archiveEntriesFromTar(TarInputStream tarStream, IGame game)
207
            => Partitioner.Create(getTarEntries(tarStream))
3✔
208
                          .AsParallel()
209
                          .Select(tuple => getArchiveEntry(tuple.Item1.Name,
3✔
210
                                                           () => tuple.Item2,
3✔
211
                                                           game,
212
                                                           tarStream.Position));
213

214
        private static IEnumerable<Tuple<TarEntry, string?>> getTarEntries(TarInputStream tarStream)
215
        {
216
            TarEntry entry;
217
            while ((entry = tarStream.GetNextEntry()) != null)
3✔
218
            {
219
                if (!entry.Name.EndsWith(".frozen"))
3✔
220
                {
221
                    yield return new Tuple<TarEntry, string?>(entry, tarStreamString(tarStream, entry));
3✔
222
                }
223
            }
224
        }
3✔
225

226
        private static string? tarStreamString(TarInputStream stream, TarEntry entry)
227
        {
228
            // Read each file into a buffer.
229
            int buffer_size;
230

231
            try
232
            {
233
                buffer_size = Convert.ToInt32(entry.Size);
3✔
234
            }
3✔
235
            catch (OverflowException)
×
236
            {
237
                log.ErrorFormat("Error processing {0}: Metadata size too large.", entry.Name);
×
238
                return null;
×
239
            }
240

241
            byte[] buffer = new byte[buffer_size];
3✔
242

243
            var len = stream.Read(buffer, 0, buffer_size);
3✔
244
            if (len != buffer_size)
3✔
245
            {
NEW
246
                throw new Exception($"TarInputStream.Read returned {len} bytes, requested {buffer_size}");
×
247
            }
248

249
            // Convert the buffer data to a string.
250
            return Encoding.UTF8.GetString(buffer);
3✔
UNCOV
251
        }
×
252

253
        private static RepositoryData FromZipStream(Stream inputStream, IGame game, IProgress<long> progress)
254
        {
255
            inputStream.Seek(0, SeekOrigin.Begin);
3✔
256
            using (var progressStream = new ReadProgressStream(inputStream, progress))
3✔
257
            using (var zipfile = new ZipFile(progressStream))
3✔
258
            {
259
                (List<CkanModule>               modules,
3✔
260
                 SortedDictionary<string, int>? counts,
261
                 GameVersion[]?                 versions,
262
                 Repository[]?                  repos,
263
                 bool                           unsupSpec) = AggregateArchiveEntries(archiveEntriesFromZip(zipfile, game));
264
                zipfile.Close();
3✔
265
                return new RepositoryData(modules, counts, versions, repos, unsupSpec);
3✔
266
            }
267
        }
3✔
268

269
        private static ParallelQuery<ArchiveEntry?> archiveEntriesFromZip(ZipFile zipfile, IGame game)
270
            => zipfile.Cast<ZipEntry>()
3✔
271
                      .ToArray()
272
                      .AsParallel()
273
                      .Select(entry => getArchiveEntry(
3✔
274
                                           entry.Name,
275
                                           () => new StreamReader(zipfile.GetInputStream(entry)).ReadToEnd(),
3✔
276
                                           game,
277
                                           entry.Offset));
278

279
        private static ArchiveList AggregateArchiveEntries(ParallelQuery<ArchiveEntry?> entries)
280
            => entries.Aggregate(new ArchiveList(new List<CkanModule>(), null, null, null, false),
3✔
281
                                 (subtotal, item) =>
282
                                    item == null
3✔
283
                                         ? subtotal
284
                                         : new ArchiveList(
285
                                             item.Item1 == null
286
                                                 ? subtotal.Item1
287
                                                 : subtotal.Item1.Append(item.Item1).ToList(),
288
                                             subtotal.Item2 ?? item.Item2,
289
                                             subtotal.Item3 ?? item.Item3,
290
                                             subtotal.Item4 ?? item.Item4,
291
                                             subtotal.Item5 || (item.Item1 == null
292
                                                                && item.Item2 == null
293
                                                                && item.Item3 == null
294
                                                                && item.Item4 == null)),
295
                                 (total, subtotal)
296
                                     => new ArchiveList(total.Item1.Concat(subtotal.Item1).ToList(),
3✔
297
                                                        total.Item2 ?? subtotal.Item2,
298
                                                        total.Item3 ?? subtotal.Item3,
299
                                                        total.Item4 ?? subtotal.Item4,
300
                                                        total.Item5 || subtotal.Item5),
301
                                 total => total);
3✔
302

303
        private static ArchiveEntry? getArchiveEntry(string        filename,
304
                                                     Func<string?> getContents,
305
                                                     IGame         game,
306
                                                     long          position)
307
            => filename.EndsWith(".ckan")
3✔
308
                ? new ArchiveEntry(ProcessRegistryMetadataFromJSON(getContents() ?? "", filename),
309
                                   null,
310
                                   null,
311
                                   null,
312
                                   position)
313
            : filename.EndsWith("download_counts.json")
314
                ? new ArchiveEntry(null,
315
                                   JsonConvert.DeserializeObject<SortedDictionary<string, int>>(getContents() ?? ""),
316
                                   null,
317
                                   null,
318
                                   position)
319
            : filename.EndsWith("builds.json")
320
                ? new ArchiveEntry(null,
321
                                   null,
322
                                   game.ParseBuildsJson(JToken.Parse(getContents() ?? "")),
323
                                   null,
324
                                   position)
325
            : filename.EndsWith("repositories.json")
326
                ? new ArchiveEntry(null,
327
                                   null,
328
                                   null,
329
                                   JObject.Parse(getContents() ?? "")
330
                                          ?["repositories"]
331
                                          ?.ToObject<Repository[]>(),
332
                                   position)
333
            : null;
334

335
        private static CkanModule? ProcessRegistryMetadataFromJSON(string metadata, string filename)
336
        {
337
            try
338
            {
339
                CkanModule module = CkanModule.FromJson(metadata);
3✔
340
                // FromJson can return null for the empty string
341
                if (module != null)
3✔
342
                {
343
                    log.DebugFormat("Module parsed: {0}", module.ToString());
3✔
344
                }
345
                return module;
3✔
346
            }
347
            catch (Exception? exception)
3✔
348
            {
349
                // Alas, we can get exceptions which *wrap* our exceptions,
350
                // because json.net seems to enjoy wrapping rather than propagating.
351
                // See KSP-CKAN/CKAN-meta#182 as to why we need to walk the whole
352
                // exception stack.
353

354
                bool handled = false;
3✔
355

356
                while (exception != null)
3✔
357
                {
358
                    if (exception is UnsupportedKraken or BadMetadataKraken)
3✔
359
                    {
360
                        // Either of these can be caused by data meant for future
361
                        // clients, so they're not really warnings, they're just
362
                        // informational.
363

364
                        log.InfoFormat("Skipping {0}: {1}", filename, exception.Message);
3✔
365

366
                        // I'd *love a way to "return" from the catch block.
367
                        handled = true;
3✔
368
                        break;
3✔
369
                    }
370

371
                    // Look further down the stack.
372
                    exception = exception.InnerException;
3✔
373
                }
374

375
                // If we haven't handled our exception, then it really was exceptional.
376
                if (!handled)
3✔
377
                {
378
                    if (exception == null)
×
379
                    {
380
                        // Had exception, walked exception tree, reached leaf, got stuck.
381
                        log.ErrorFormat("Error processing {0} (exception tree leaf)", filename);
×
382
                    }
383
                    else
384
                    {
385
                        // In case whatever's calling us is lazy in error reporting, we'll
386
                        // report that we've got an issue here.
387
                        log.ErrorFormat("Error processing {0}: {1}", filename, exception.Message);
×
388
                    }
389

390
                    throw;
×
391
                }
392
                return null;
3✔
393
            }
394
        }
3✔
395

396
        private static readonly ILog log = LogManager.GetLogger(typeof(RepositoryData));
3✔
397
    }
398
}
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