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

KSP-CKAN / CKAN / 20049808028

09 Dec 2025 02:28AM UTC coverage: 85.3% (-0.04%) from 85.335%
20049808028

push

github

HebaruSan
Merge #4469 Refactor module installer to use relative paths internally

1998 of 2162 branches covered (92.41%)

Branch coverage included in aggregate %.

87 of 98 new or added lines in 10 files covered. (88.78%)

1 existing line in 1 file now uncovered.

11928 of 14164 relevant lines covered (84.21%)

1.76 hits per line

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

85.26
/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 ICSharpCode.SharpZipLib.Zip;
8
using Newtonsoft.Json;
9
using Newtonsoft.Json.Linq;
10
using log4net;
11

12
using CKAN.IO;
13
using CKAN.Extensions;
14
using CKAN.Avc;
15
using CKAN.Games;
16

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

26
        private readonly IGame game;
27

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

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

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

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

71
        public bool HasInstallableFiles(CkanModule module, string filePath)
72
            => Utilities.DefaultIfThrows(() =>
2✔
73
                   ModuleInstaller.FindInstallableFiles(module, filePath, game).ToArray())
2✔
74
                           != null;
75

76
        public IEnumerable<InstallableFile> GetConfigFiles(CkanModule module, ZipFile zip)
77
            => GetFilesBySuffix(module, zip, ".cfg", game);
2✔
78

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

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

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

92
        public IEnumerable<InstallableFile> GetSourceCode(CkanModule module, ZipFile zip)
93
            => GetFilesBySuffixes(module, zip, sourceCodeSuffixes, game);
2✔
94

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

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

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

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

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

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

147
            const string versionExt = ".version";
148

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

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

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

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

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

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

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

232
        public IEnumerable<string> GetInternalSpaceWarpInfos(CkanModule module,
233
                                                             ZipFile    zip,
234
                                                             string?    internalFilePath = null)
235
            => (internalFilePath is { Length: > 0 }
2✔
236
                    ? ModuleInstaller.FindInstallableFiles(module, zip, game)
NEW
237
                                     .Where(instF => instF.source.Name == internalFilePath)
×
238
                    : GetFilesBySuffix(module, zip, SpaceWarpInfoFilename, game))
239
                .Select(instF => instF.source)
2✔
240
                .Select(entry => new StreamReader(zip.GetInputStream(entry)).ReadToEnd());
2✔
241

242
        private const string SpaceWarpInfoFilename = "swinfo.json";
243
    }
244
}
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