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

KSP-CKAN / CKAN / 16536075576

26 Jul 2025 04:27AM UTC coverage: 56.351% (+8.5%) from 47.804%
16536075576

push

github

HebaruSan
Merge #4408 Add tests for CmdLine

4558 of 8422 branches covered (54.12%)

Branch coverage included in aggregate %.

148 of 273 new or added lines in 28 files covered. (54.21%)

18 existing lines in 5 files now uncovered.

9719 of 16914 relevant lines covered (57.46%)

1.18 hits per line

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

44.78
/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)
2✔
24
        {
2✔
25
            this.game = game;
2✔
26
        }
2✔
27

28
        private readonly IGame game;
29

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

32
        public AvcVersion? GetInternalAvc(CkanModule module, string zipFilePath, string? internalFilePath = null)
33
        {
2✔
34
            using (var zipfile = new ZipFile(zipFilePath))
2✔
35
            {
2✔
36
                return GetInternalAvc(zipfile, FindInternalAvc(module, zipfile, internalFilePath)?.Item1);
2✔
37
            }
38
        }
2✔
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);
2✔
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 static JObject? GetInternalCkan(CkanModule module, ZipFile zip, GameInstance inst)
62
            => (module.install != null
2✔
63
                    // Find embedded .ckan files that would be included in the install
64
                    ? GetFilesBySuffix(module, zip, ".ckan", inst)
65
                        .Select(instF => instF.source)
2✔
66
                    // Find embedded .ckan files anywhere in the ZIP
67
                    : zip.OfType<ZipEntry>()
68
                         .Where(ModuleInstaller.IsInternalCkan))
69
                .Select(entry => DeserializeFromStream(
2✔
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(() =>
2✔
76
                   ModuleInstaller.FindInstallableFiles(module, filePath,
2✔
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 static IEnumerable<InstallableFile> GetFilesBySuffix(CkanModule   module,
90
                                                                     ZipFile      zip,
91
                                                                     string       suffix,
92
                                                                     GameInstance inst)
93
            => ModuleInstaller.FindInstallableFiles(module, zip, inst)
2✔
94
                              .Where(instF => instF.destination.EndsWith(suffix, StringComparison.InvariantCultureIgnoreCase));
2✔
95

96
        public IEnumerable<InstallableFile> GetSourceCode(CkanModule module, ZipFile zip, GameInstance inst)
97
            => GetFilesBySuffixes(module, zip, sourceCodeSuffixes, inst);
×
98

99
        private static readonly string[] sourceCodeSuffixes = new string[] { ".cs", ".csproj", ".sln" };
2✔
100

101
        private static IEnumerable<InstallableFile> GetFilesBySuffixes(CkanModule          module,
102
                                                                       ZipFile             zip,
103
                                                                       ICollection<string> suffixes,
104
                                                                       GameInstance        inst)
105
            => ModuleInstaller.FindInstallableFiles(module, zip, inst)
×
106
                              .Where(instF => suffixes.Any(suffix => instF.destination.EndsWith(suffix, StringComparison.InvariantCultureIgnoreCase)));
×
107

108
        public IEnumerable<ZipEntry> FileSources(CkanModule module, ZipFile zip, GameInstance inst)
109
            => ModuleInstaller.FindInstallableFiles(module, zip, inst)
2✔
110
                              .Select(instF => instF.source)
2✔
111
                              .Where(ze => !ze.IsDirectory);
2✔
112

113
        public IEnumerable<string> FileDestinations(CkanModule module, string filePath)
114
        {
×
115
            var inst = new GameInstance(game, "/", "dummy", null);
×
116
            return ModuleInstaller
×
117
                .FindInstallableFiles(module, filePath, inst)
118
                .Where(f => !f.source.IsDirectory)
×
119
                .Select(f => inst.ToRelativeGameDir(f.destination));
×
120
        }
×
121

122
        /// <summary>
123
        /// Return a parsed JObject from a stream.
124
        /// Courtesy https://stackoverflow.com/questions/8157636/can-json-net-serialize-deserialize-to-from-a-stream/17788118#17788118
125
        /// </summary>
126
        private static JObject? DeserializeFromStream(Stream stream)
127
        {
2✔
128
            using (var sr = new StreamReader(stream))
2✔
129
            {
2✔
130
                // Only one document per internal .ckan
131
                return YamlExtensions.Parse(sr).FirstOrDefault()?.ToJObject();
2✔
132
            }
133
        }
2✔
134

135
        /// <summary>
136
        /// Locate a version file in an archive.
137
        /// This requires a module object as we *first* search files we might install,
138
        /// falling back to a search of all files in the archive.
139
        /// Returns null if no version is found.
140
        /// Throws a Kraken if too many versions are found.
141
        /// </summary>
142
        /// <param name="module">The metadata associated with this module, used to find installable files</param>
143
        /// <param name="zipfile">The archive containing the module's files</param>
144
        /// <param name="internalFilePath">Filter for selecting a version file, either exact match or regular expression</param>
145
        /// <returns>
146
        /// Tuple consisting of the chosen file's entry in the archive plus a boolean
147
        /// indicating whether it's a file would be extracted to disk at installation
148
        /// </returns>
149
        public Tuple<ZipEntry, bool>? FindInternalAvc(CkanModule module,
150
                                                      ZipFile    zipfile,
151
                                                      string?    internalFilePath)
152
        {
2✔
153
            Log.DebugFormat("Finding AVC .version file for {0}", module);
2✔
154

155
            const string versionExt = ".version";
156

157
            // Get all our version files
158
            var ksp = new GameInstance(game, "/", "dummy", new NullUser());
2✔
159
            var files = ModuleInstaller.FindInstallableFiles(module, zipfile, ksp)
2✔
160
                .Select(x => x.source)
2✔
161
                .Where(source => source.Name.EndsWith(versionExt,
2✔
162
                    StringComparison.InvariantCultureIgnoreCase))
163
                .ToList();
164
            // By default, we look for ones we can install
165
            var installable = true;
2✔
166

167
            if (files.Count == 0)
2!
168
            {
×
169
                // Oh dear, no version file at all? Let's see if we can find *any* to use.
170
                files.AddRange(zipfile.Cast<ZipEntry>()
×
171
                    .Where(file => file.Name.EndsWith(versionExt,
×
172
                        StringComparison.InvariantCultureIgnoreCase)));
173

174
                if (files.Count == 0)
×
175
                {
×
176
                    // Okay, there's *really* nothing there.
177
                    return null;
×
178
                }
179
                // Tell calling code that it may not be a "real" version file
180
                installable = false;
×
181
            }
×
182

183
            if (!string.IsNullOrWhiteSpace(internalFilePath))
2!
184
            {
×
185
                Regex internalRE = new Regex(internalFilePath, RegexOptions.Compiled);
×
186
                var avcEntry = files
×
187
                    .FirstOrDefault(f => f.Name == internalFilePath || internalRE.IsMatch(f.Name));
×
188
                if (avcEntry == null)
×
189
                {
×
190
                    throw new Kraken(
×
191
                        string.Format("Invalid $vref path/regexp {0}, doesn't match any of: {1}",
192
                                      internalFilePath,
193
                                      string.Join(", ", files.Select(f => f.Name))));
×
194
                }
195
                return new Tuple<ZipEntry, bool>(avcEntry, installable);
×
196
            }
197
            else if (files.Count > 1)
2!
198
            {
×
199
                throw new Kraken(
×
200
                    string.Format("Too many .version files located: {0}",
NEW
201
                              string.Join(", ", files.Select(x => x.Name).Order())));
×
202
            }
203
            else
204
            {
2✔
205
                return new Tuple<ZipEntry, bool>(files.First(), installable);
2✔
206
            }
207
        }
2✔
208

209
        /// <summary>
210
        /// Returns an AVC object for the given file in the archive, if any.
211
        /// </summary>
212
        public static AvcVersion? GetInternalAvc(ZipFile zipfile, ZipEntry? avcEntry)
213
        {
2✔
214
            if (avcEntry == null)
2!
215
            {
×
216
                return null;
×
217
            }
218
            Log.DebugFormat("Using AVC data from {0}", avcEntry.Name);
2✔
219

220
            // Hooray, found our entry. Extract and return it.
221
            using (var zipstream = zipfile.GetInputStream(avcEntry))
2✔
222
            using (var stream = new StreamReader(zipstream))
2✔
223
            {
2✔
224
                var json = stream.ReadToEnd();
2✔
225

226
                Log.DebugFormat("Parsing {0}", json);
2✔
227
                try
228
                {
2✔
229
                    return JsonConvert.DeserializeObject<AvcVersion>(json);
2✔
230
                }
231
                catch (JsonException exc)
×
232
                {
×
233
                    throw new Kraken(string.Format(
×
234
                        "Error parsing version file {0}: {1}",
235
                        avcEntry.Name,
236
                        exc.Message));
237
                }
238
            }
239
        }
2✔
240

241
        private const string SpaceWarpInfoFilename = "swinfo.json";
242
        private static readonly JsonSerializerSettings ignoreJsonErrors = new JsonSerializerSettings()
2✔
243
        {
244
            DateTimeZoneHandling = DateTimeZoneHandling.Utc,
245
            Error = (sender, e) => e.ErrorContext.Handled = true
×
246
        };
247

248
        public SpaceWarpInfo? ParseSpaceWarpJson(string? json)
249
            => json == null ? null : JsonConvert.DeserializeObject<SpaceWarpInfo>(json, ignoreJsonErrors);
×
250

251
        public SpaceWarpInfo? GetInternalSpaceWarpInfo(CkanModule   module,
252
                                               ZipFile      zip,
253
                                               GameInstance inst,
254
                                               string?      internalFilePath = null)
255
            => GetInternalSpaceWarpInfos(module, zip, inst, internalFilePath).FirstOrDefault();
×
256

257
        private IEnumerable<SpaceWarpInfo> GetInternalSpaceWarpInfos(CkanModule   module,
258
                                                                     ZipFile      zip,
259
                                                                     GameInstance inst,
260
                                                                     string?      internalFilePath = null)
261
            => (string.IsNullOrWhiteSpace(internalFilePath)
×
262
                    ? GetFilesBySuffix(module, zip, SpaceWarpInfoFilename, inst)
263
                    : ModuleInstaller.FindInstallableFiles(module, zip, inst)
264
                                     .Where(instF => instF.source.Name == internalFilePath))
×
265
                .Select(instF => instF.source)
×
266
                .Select(entry => ParseSpaceWarpJson(new StreamReader(zip.GetInputStream(entry)).ReadToEnd()))
×
267
                .OfType<SpaceWarpInfo>();
268

269
        public SpaceWarpInfo? GetSpaceWarpInfo(CkanModule   module,
270
                                               ZipFile      zip,
271
                                               GameInstance inst,
272
                                               IGithubApi   githubApi,
273
                                               IHttpService httpSvc,
274
                                               string?      internalFilePath = null)
275
            => GetInternalSpaceWarpInfos(module, zip, inst, internalFilePath)
×
276
               .Select(swinfo => swinfo.version_check != null
×
277
                                 && Uri.IsWellFormedUriString(swinfo.version_check.OriginalString, UriKind.Absolute)
278
                                 && ParseSpaceWarpJson(githubApi?.DownloadText(swinfo.version_check)
279
                                                                ?? httpSvc.DownloadText(swinfo.version_check))
280
                                    is SpaceWarpInfo remoteSwinfo
281
                                 && remoteSwinfo.version == swinfo.version
282
                                     ? remoteSwinfo
283
                                     : swinfo)
284
               .FirstOrDefault();
285
    }
286
}
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