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

KSP-CKAN / CKAN / 17904669173

22 Sep 2025 04:34AM UTC coverage: 75.604% (+1.2%) from 74.397%
17904669173

push

github

HebaruSan
Merge #4443 Report number of filtered files in install

5231 of 7236 branches covered (72.29%)

Branch coverage included in aggregate %.

192 of 218 new or added lines in 41 files covered. (88.07%)

35 existing lines in 7 files now uncovered.

11163 of 14448 relevant lines covered (77.26%)

1.58 hits per line

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

74.02
/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)
50
            => GetInternalCkan(module, new ZipFile(zipPath), game);
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, IGame game)
62
            => (module.install != null
2✔
63
                    // Find embedded .ckan files that would be included in the install
64
                    ? GetFilesBySuffix(module, zip, ".ckan", game)
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, game))
2✔
77
                           != null;
78

79
        public IEnumerable<InstallableFile> GetConfigFiles(CkanModule module, ZipFile zip)
80
            => GetFilesBySuffix(module, zip, ".cfg", game);
2✔
81

82
        public IEnumerable<InstallableFile> GetPlugins(CkanModule module, ZipFile zip)
83
            => GetFilesBySuffix(module, zip, ".dll", game);
2✔
84

85
        public IEnumerable<InstallableFile> GetCrafts(CkanModule module, ZipFile zip)
86
            => GetFilesBySuffix(module, zip, ".craft", game);
2✔
87

88
        private static IEnumerable<InstallableFile> GetFilesBySuffix(CkanModule   module,
89
                                                                     ZipFile      zip,
90
                                                                     string       suffix,
91
                                                                     IGame        game)
92
            => ModuleInstaller.FindInstallableFiles(module, zip, game)
2✔
93
                              .Where(instF => instF.destination.EndsWith(suffix, StringComparison.InvariantCultureIgnoreCase));
2✔
94

95
        public IEnumerable<InstallableFile> GetSourceCode(CkanModule module, ZipFile zip)
96
            => GetFilesBySuffixes(module, zip, sourceCodeSuffixes, game);
2✔
97

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

100
        private static IEnumerable<InstallableFile> GetFilesBySuffixes(CkanModule                  module,
101
                                                                       ZipFile                     zip,
102
                                                                       IReadOnlyCollection<string> suffixes,
103
                                                                       IGame                       game)
104
            => ModuleInstaller.FindInstallableFiles(module, zip, game)
2✔
105
                              .Where(instF => suffixes.Any(suffix => instF.destination.EndsWith(suffix, StringComparison.InvariantCultureIgnoreCase)));
2✔
106

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

112
        public IEnumerable<string> FileDestinations(CkanModule module, string filePath)
113
            => ModuleInstaller.FindInstallableFiles(module, filePath, game)
2✔
114
                              .Where(f => !f.source.IsDirectory)
2✔
115
                              .Select(f => f.destination);
2✔
116

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

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

150
            const string versionExt = ".version";
151

152
            // Get all our version files
153
            var files = ModuleInstaller.FindInstallableFiles(module, zipfile, game)
2✔
154
                .Select(x => x.source)
2✔
155
                .Where(source => source.Name.EndsWith(versionExt,
2✔
156
                    StringComparison.InvariantCultureIgnoreCase))
157
                .ToList();
158
            // By default, we look for ones we can install
159
            var installable = true;
2✔
160

161
            if (files.Count == 0)
2!
162
            {
×
163
                // Oh dear, no version file at all? Let's see if we can find *any* to use.
164
                files.AddRange(zipfile.Cast<ZipEntry>()
×
165
                    .Where(file => file.Name.EndsWith(versionExt,
×
166
                        StringComparison.InvariantCultureIgnoreCase)));
167

168
                if (files.Count == 0)
×
169
                {
×
170
                    // Okay, there's *really* nothing there.
171
                    return null;
×
172
                }
173
                // Tell calling code that it may not be a "real" version file
174
                installable = false;
×
175
            }
×
176

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

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

214
            // Hooray, found our entry. Extract and return it.
215
            using (var zipstream = zipfile.GetInputStream(avcEntry))
2✔
216
            using (var stream = new StreamReader(zipstream))
2✔
217
            {
2✔
218
                var json = stream.ReadToEnd();
2✔
219

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

235
        private const string SpaceWarpInfoFilename = "swinfo.json";
236
        private static readonly JsonSerializerSettings ignoreJsonErrors = new JsonSerializerSettings()
2✔
237
        {
238
            DateTimeZoneHandling = DateTimeZoneHandling.Utc,
239
            Error = (sender, e) => e.ErrorContext.Handled = true
×
240
        };
241

242
        public SpaceWarpInfo? ParseSpaceWarpJson(string? json)
243
            => json == null ? null : JsonConvert.DeserializeObject<SpaceWarpInfo>(json, ignoreJsonErrors);
2✔
244

245
        public SpaceWarpInfo? GetInternalSpaceWarpInfo(CkanModule   module,
246
                                                       ZipFile      zip,
247
                                                       string?      internalFilePath = null)
248
            => GetInternalSpaceWarpInfos(module, zip, internalFilePath).FirstOrDefault();
2✔
249

250
        private IEnumerable<SpaceWarpInfo> GetInternalSpaceWarpInfos(CkanModule   module,
251
                                                                     ZipFile      zip,
252
                                                                     string?      internalFilePath = null)
253
            => (string.IsNullOrWhiteSpace(internalFilePath)
2✔
254
                    ? GetFilesBySuffix(module, zip, SpaceWarpInfoFilename, game)
255
                    : ModuleInstaller.FindInstallableFiles(module, zip, game)
UNCOV
256
                                     .Where(instF => instF.source.Name == internalFilePath))
×
257
                .Select(instF => instF.source)
2✔
258
                .Select(entry => ParseSpaceWarpJson(new StreamReader(zip.GetInputStream(entry)).ReadToEnd()))
2✔
259
                .OfType<SpaceWarpInfo>();
260

261
        public SpaceWarpInfo? GetSpaceWarpInfo(CkanModule   module,
262
                                               ZipFile      zip,
263
                                               IGithubApi   githubApi,
264
                                               IHttpService httpSvc,
265
                                               string?      internalFilePath = null)
266
            => GetInternalSpaceWarpInfos(module, zip, internalFilePath)
2✔
267
               .Select(swinfo => swinfo.version_check != null
2!
268
                                 && Uri.IsWellFormedUriString(swinfo.version_check.OriginalString, UriKind.Absolute)
269
                                 && ParseSpaceWarpJson(githubApi?.DownloadText(swinfo.version_check)
270
                                                                ?? httpSvc.DownloadText(swinfo.version_check))
271
                                    is SpaceWarpInfo remoteSwinfo
272
                                 && remoteSwinfo.version == swinfo.version
273
                                     ? remoteSwinfo
274
                                     : swinfo)
275
               .FirstOrDefault();
276
    }
277
}
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