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

KSP-CKAN / CKAN / 15833572503

23 Jun 2025 07:42PM UTC coverage: 42.236% (+0.1%) from 42.099%
15833572503

push

github

HebaruSan
Merge #4398 Exception handling revamp, parallel multi-host inflation

3881 of 9479 branches covered (40.94%)

Branch coverage included in aggregate %.

48 of 137 new or added lines in 30 files covered. (35.04%)

12 existing lines in 6 files now uncovered.

8334 of 19442 relevant lines covered (42.87%)

3.51 hits per line

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

45.38
/Netkan/Services/ModuleService.cs
1
using System;
2
using System.IO;
3
using System.Linq;
4
using System.Collections.Generic;
5
using System.Text.RegularExpressions;
6

7
using log4net;
8
using ICSharpCode.SharpZipLib.Zip;
9
using Newtonsoft.Json;
10
using Newtonsoft.Json.Linq;
11

12
using CKAN.IO;
13
using CKAN.Extensions;
14
using CKAN.Avc;
15
using CKAN.SpaceWarp;
16
using CKAN.Games;
17
using CKAN.NetKAN.Sources.Github;
18

19
namespace CKAN.NetKAN.Services
20
{
21
    internal sealed class ModuleService : IModuleService
22
    {
23
        public ModuleService(IGame game)
8✔
24
        {
8✔
25
            this.game = game;
8✔
26
        }
8✔
27

28
        private readonly IGame game;
29

30
        private static readonly ILog Log = LogManager.GetLogger(typeof(ModuleService));
8✔
31

32
        public AvcVersion? GetInternalAvc(CkanModule module, string zipFilePath, string? internalFilePath = null)
33
        {
8✔
34
            using (var zipfile = new ZipFile(zipFilePath))
8✔
35
            {
8✔
36
                return GetInternalAvc(zipfile, FindInternalAvc(module, zipfile, internalFilePath)?.Item1);
8✔
37
            }
38
        }
8✔
39

40
        /// <summary>
41
        /// Find and parse a .ckan file in a ZIP.
42
        /// If the module has an `install` property, only files that would
43
        /// be installed are considered. Otherwise the whole ZIP is searched.
44
        /// </summary>
45
        /// <param name="module">The CkanModule associated with the ZIP, so we can tell which files would be installed</param>
46
        /// <param name="zipPath">Where the ZIP file is</param>
47
        /// <param name="inst">Game instance for generating InstallableFiles</param>
48
        /// <returns>Parsed contents of the file, or null if none found</returns>
49
        public JObject? GetInternalCkan(CkanModule module, string zipPath, GameInstance inst)
50
            => GetInternalCkan(module, new ZipFile(zipPath), inst);
8✔
51

52
        /// <summary>
53
        /// Find and parse a .ckan file in the ZIP.
54
        /// If the module has an `install` property, only files that would
55
        /// be installed are considered. Otherwise the whole ZIP is searched.
56
        /// </summary>
57
        /// <param name="module">The CkanModule associated with the ZIP, so we can tell which files would be installed</param>
58
        /// <param name="zip">The ZipFile to search</param>
59
        /// <param name="inst">Game instance for generating InstallableFiles</param>
60
        /// <returns>Parsed contents of the file, or null if none found</returns>
61
        private JObject? GetInternalCkan(CkanModule module, ZipFile zip, GameInstance inst)
62
            => (module.install != null
8✔
63
                    // Find embedded .ckan files that would be included in the install
64
                    ? GetFilesBySuffix(module, zip, ".ckan", inst)
65
                        .Select(instF => instF.source)
8✔
66
                    // Find embedded .ckan files anywhere in the ZIP
67
                    : zip.OfType<ZipEntry>()
68
                         .Where(ModuleInstaller.IsInternalCkan))
69
                .Select(entry => DeserializeFromStream(
8✔
70
                                    zip.GetInputStream(entry)))
71
                .FirstOrDefault();
72

73
        public bool HasInstallableFiles(CkanModule module, string filePath)
74
            // TODO: DBB: Let's not use exceptions for flow control
75
            => Utilities.DefaultIfThrows(() =>
8✔
76
                   ModuleInstaller.FindInstallableFiles(module, filePath,
8✔
77
                       new GameInstance(game, "/", "dummy", new NullUser())))
78
                           != null;
79

80
        public IEnumerable<InstallableFile> GetConfigFiles(CkanModule module, ZipFile zip, GameInstance inst)
81
            => GetFilesBySuffix(module, zip, ".cfg", inst);
×
82

83
        public IEnumerable<InstallableFile> GetPlugins(CkanModule module, ZipFile zip, GameInstance inst)
84
            => GetFilesBySuffix(module, zip, ".dll", inst);
×
85

86
        public IEnumerable<InstallableFile> GetCrafts(CkanModule module, ZipFile zip, GameInstance inst)
87
            => GetFilesBySuffix(module, zip, ".craft", inst);
×
88

89
        private IEnumerable<InstallableFile> GetFilesBySuffix(CkanModule module, ZipFile zip, string suffix, GameInstance inst)
90
            => ModuleInstaller.FindInstallableFiles(module, zip, inst)
8✔
91
                              .Where(instF => instF.destination.EndsWith(suffix, StringComparison.InvariantCultureIgnoreCase));
8✔
92

93
        public IEnumerable<ZipEntry> FileSources(CkanModule module, ZipFile zip, GameInstance inst)
94
            => ModuleInstaller.FindInstallableFiles(module, zip, inst)
8✔
95
                              .Select(instF => instF.source)
8✔
96
                              .Where(ze => !ze.IsDirectory);
8✔
97

98
        public IEnumerable<string> FileDestinations(CkanModule module, string filePath)
99
        {
×
100
            var inst = new GameInstance(game, "/", "dummy", null);
×
101
            return ModuleInstaller
×
102
                .FindInstallableFiles(module, filePath, inst)
103
                .Where(f => !f.source.IsDirectory)
×
104
                .Select(f => inst.ToRelativeGameDir(f.destination));
×
105
        }
×
106

107
        /// <summary>
108
        /// Return a parsed JObject from a stream.
109
        /// Courtesy https://stackoverflow.com/questions/8157636/can-json-net-serialize-deserialize-to-from-a-stream/17788118#17788118
110
        /// </summary>
111
        private static JObject? DeserializeFromStream(Stream stream)
112
        {
8✔
113
            using (var sr = new StreamReader(stream))
8✔
114
            {
8✔
115
                // Only one document per internal .ckan
116
                return YamlExtensions.Parse(sr).FirstOrDefault()?.ToJObject();
8✔
117
            }
118
        }
8✔
119

120
        /// <summary>
121
        /// Locate a version file in an archive.
122
        /// This requires a module object as we *first* search files we might install,
123
        /// falling back to a search of all files in the archive.
124
        /// Returns null if no version is found.
125
        /// Throws a Kraken if too many versions are found.
126
        /// </summary>
127
        /// <param name="module">The metadata associated with this module, used to find installable files</param>
128
        /// <param name="zipfile">The archive containing the module's files</param>
129
        /// <param name="internalFilePath">Filter for selecting a version file, either exact match or regular expression</param>
130
        /// <returns>
131
        /// Tuple consisting of the chosen file's entry in the archive plus a boolean
132
        /// indicating whether it's a file would be extracted to disk at installation
133
        /// </returns>
134
        public Tuple<ZipEntry, bool>? FindInternalAvc(CkanModule module,
135
                                                      ZipFile    zipfile,
136
                                                      string?    internalFilePath)
137
        {
8✔
138
            Log.DebugFormat("Finding AVC .version file for {0}", module);
8✔
139

140
            const string versionExt = ".version";
141

142
            // Get all our version files
143
            var ksp = new GameInstance(game, "/", "dummy", new NullUser());
8✔
144
            var files = ModuleInstaller.FindInstallableFiles(module, zipfile, ksp)
8✔
145
                .Select(x => x.source)
8✔
146
                .Where(source => source.Name.EndsWith(versionExt,
8✔
147
                    StringComparison.InvariantCultureIgnoreCase))
148
                .ToList();
149
            // By default, we look for ones we can install
150
            var installable = true;
8✔
151

152
            if (files.Count == 0)
8!
153
            {
×
154
                // Oh dear, no version file at all? Let's see if we can find *any* to use.
155
                files.AddRange(zipfile.Cast<ZipEntry>()
×
156
                    .Where(file => file.Name.EndsWith(versionExt,
×
157
                        StringComparison.InvariantCultureIgnoreCase)));
158

159
                if (files.Count == 0)
×
160
                {
×
161
                    // Okay, there's *really* nothing there.
162
                    return null;
×
163
                }
164
                // Tell calling code that it may not be a "real" version file
165
                installable = false;
×
166
            }
×
167

168
            if (!string.IsNullOrWhiteSpace(internalFilePath))
8!
169
            {
×
170
                Regex internalRE = new Regex(internalFilePath, RegexOptions.Compiled);
×
171
                var avcEntry = files
×
172
                    .FirstOrDefault(f => f.Name == internalFilePath || internalRE.IsMatch(f.Name));
×
173
                if (avcEntry == null)
×
174
                {
×
175
                    throw new Kraken(
×
176
                        string.Format("Invalid $vref path/regexp {0}, doesn't match any of: {1}",
177
                                      internalFilePath,
NEW
178
                                      string.Join(", ", files.Select(f => f.Name))));
×
179
                }
180
                return new Tuple<ZipEntry, bool>(avcEntry, installable);
×
181
            }
182
            else if (files.Count > 1)
8!
183
            {
×
184
                throw new Kraken(
×
185
                    string.Format("Too many .version files located: {0}",
186
                              string.Join(", ", files.Select(x => x.Name).OrderBy(f => f))));
×
187
            }
188
            else
189
            {
8✔
190
                return new Tuple<ZipEntry, bool>(files.First(), installable);
8✔
191
            }
192
        }
8✔
193

194
        /// <summary>
195
        /// Returns an AVC object for the given file in the archive, if any.
196
        /// </summary>
197
        public static AvcVersion? GetInternalAvc(ZipFile zipfile, ZipEntry? avcEntry)
198
        {
8✔
199
            if (avcEntry == null)
8!
200
            {
×
201
                return null;
×
202
            }
203
            Log.DebugFormat("Using AVC data from {0}", avcEntry.Name);
8✔
204

205
            // Hooray, found our entry. Extract and return it.
206
            using (var zipstream = zipfile.GetInputStream(avcEntry))
8✔
207
            using (var stream = new StreamReader(zipstream))
8✔
208
            {
8✔
209
                var json = stream.ReadToEnd();
8✔
210

211
                Log.DebugFormat("Parsing {0}", json);
8✔
212
                try
213
                {
8✔
214
                    return JsonConvert.DeserializeObject<AvcVersion>(json);
8✔
215
                }
216
                catch (JsonException exc)
×
217
                {
×
218
                    throw new Kraken(string.Format(
×
219
                        "Error parsing version file {0}: {1}",
220
                        avcEntry.Name,
221
                        exc.Message));
222
                }
223
            }
224
        }
8✔
225

226
        private const string SpaceWarpInfoFilename = "swinfo.json";
227
        private static readonly JsonSerializerSettings ignoreJsonErrors = new JsonSerializerSettings()
8✔
228
        {
229
            DateTimeZoneHandling = DateTimeZoneHandling.Utc,
230
            Error = (sender, e) => e.ErrorContext.Handled = true
×
231
        };
232

233
        public SpaceWarpInfo? ParseSpaceWarpJson(string? json)
234
            => json == null ? null : JsonConvert.DeserializeObject<SpaceWarpInfo>(json, ignoreJsonErrors);
×
235

236
        public SpaceWarpInfo? GetInternalSpaceWarpInfo(CkanModule   module,
237
                                               ZipFile      zip,
238
                                               GameInstance inst,
239
                                               string?      internalFilePath = null)
240
            => GetInternalSpaceWarpInfos(module, zip, inst, internalFilePath).FirstOrDefault();
×
241

242
        private IEnumerable<SpaceWarpInfo> GetInternalSpaceWarpInfos(CkanModule   module,
243
                                                                     ZipFile      zip,
244
                                                                     GameInstance inst,
245
                                                                     string?      internalFilePath = null)
246
            => (string.IsNullOrWhiteSpace(internalFilePath)
×
247
                    ? GetFilesBySuffix(module, zip, SpaceWarpInfoFilename, inst)
248
                    : ModuleInstaller.FindInstallableFiles(module, zip, inst)
249
                                     .Where(instF => instF.source.Name == internalFilePath))
×
250
                .Select(instF => instF.source)
×
251
                .Select(entry => ParseSpaceWarpJson(new StreamReader(zip.GetInputStream(entry)).ReadToEnd()))
×
252
                .OfType<SpaceWarpInfo>();
253

254
        public SpaceWarpInfo? GetSpaceWarpInfo(CkanModule   module,
255
                                               ZipFile      zip,
256
                                               GameInstance inst,
257
                                               IGithubApi   githubApi,
258
                                               IHttpService httpSvc,
259
                                               string?      internalFilePath = null)
260
            => GetInternalSpaceWarpInfos(module, zip, inst, internalFilePath)
×
261
               .Select(swinfo => swinfo.version_check != null
×
262
                                 && Uri.IsWellFormedUriString(swinfo.version_check.OriginalString, UriKind.Absolute)
263
                                 && ParseSpaceWarpJson(githubApi?.DownloadText(swinfo.version_check)
264
                                                                ?? httpSvc.DownloadText(swinfo.version_check))
265
                                    is SpaceWarpInfo remoteSwinfo
266
                                 && remoteSwinfo.version == swinfo.version
267
                                     ? remoteSwinfo
268
                                     : swinfo)
269
               .FirstOrDefault();
270
    }
271
}
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