• 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

88.43
/Core/Net/NetModuleCache.cs
1
using System;
2
using System.Linq;
3
using System.IO;
4
using System.Threading;
5
using System.Collections.Generic;
6

7
using ICSharpCode.SharpZipLib.Zip;
8

9
using CKAN.IO;
10
using CKAN.Configuration;
11

12
namespace CKAN
13
{
14
    /// <summary>
15
    /// A cache object that protects the validity of the files it contains.
16
    /// A CkanModule must be provided for each file added, and the following
17
    /// properties are checked before adding:
18
    ///   - CkanModule.download_size
19
    ///   - CkanModule.download_hash.sha1
20
    ///   - CkanModule.download_hash.sha256
21
    /// </summary>
22
    public class NetModuleCache : IDisposable
23
    {
24
        /// <summary>
25
        /// Initialize the cache
26
        /// </summary>
27
        /// <param name="mgr">GameInstanceManager containing instances that might have legacy caches</param>
28
        /// <param name="path">Path to directory to use as the cache</param>
29
        public NetModuleCache(GameInstanceManager mgr, string path)
3✔
30
        {
31
            cache = new NetFileCache(mgr, path);
3✔
32
        }
3✔
33

34
        /// <summary>
35
        /// Initialize the cache
36
        /// </summary>
37
        /// <param name="path">Path to directory to use as the cache</param>
38
        public NetModuleCache(string path)
3✔
39
        {
40
            cache = new NetFileCache(path);
3✔
41
        }
3✔
42

43
        public event Action<CkanModule>?  ModStored;
44
        public event Action<CkanModule?>? ModPurged;
45

46
        // Simple passthrough wrappers
47
        public void Dispose()
48
        {
49
            cache.Dispose();
3✔
50
            GC.SuppressFinalize(this);
3✔
51
        }
3✔
52
        public void RemoveAll()
53
        {
54
            cache.RemoveAll();
3✔
55
            ModPurged?.Invoke(null);
3✔
UNCOV
56
        }
×
57
        public void MoveFrom(DirectoryInfo fromDir, IProgress<int> progress)
58
        {
59
            cache.MoveFrom(fromDir, progress);
×
60
        }
×
61
        public bool IsCached(CkanModule m)
62
            => m.download?.Any(cache.IsCached)
3✔
63
                ?? false;
64
        public bool IsMaybeCachedZip(CkanModule m)
65
            => m.download?.Any(dlUri => cache.IsMaybeCachedZip(dlUri, m.release_date))
3✔
66
                ?? false;
67
        public string? GetCachedFilename(CkanModule m)
68
            => m.download?.Select(dlUri => cache.GetCachedFilename(dlUri, m.release_date))
3✔
69
                          .FirstOrDefault(filename => filename != null);
3✔
70
        public void GetSizeInfo(out int numFiles, out long numBytes, out long? bytesFree)
71
        {
72
            cache.GetSizeInfo(out numFiles, out numBytes, out bytesFree);
3✔
73
        }
3✔
74
        public void EnforceSizeLimit(long bytes, Registry registry)
75
        {
76
            cache.EnforceSizeLimit(bytes, registry);
3✔
77
        }
3✔
78
        public void CheckFreeSpace(long bytesToStore)
79
        {
80
            cache.CheckFreeSpace(bytesToStore);
3✔
81
        }
3✔
82

83
        public FileInfo? GetInProgressFileName(CkanModule m)
84
            => m.download == null
3✔
85
                ? null
86
                : cache.GetInProgressFileName(m.download, m.StandardName());
87

88
        private static string DescribeUncachedAvailability(IConfiguration config,
89
                                                           CkanModule     m,
90
                                                           FileInfo?      fi)
91
            => (fi?.Exists ?? false)
3✔
92
                ? string.Format(Properties.Resources.NetModuleCacheModuleResuming,
93
                    m.name, m.version,
94
                    string.Join(", ", ModuleInstaller.PrioritizedHosts(config, m.download)),
95
                    CkanModule.FmtSize(m.download_size - fi.Length))
96
                : string.Format(Properties.Resources.NetModuleCacheModuleHostSize,
97
                    m.name, m.version,
98
                    string.Join(", ", ModuleInstaller.PrioritizedHosts(config, m.download)),
99
                    CkanModule.FmtSize(m.download_size));
100

101
        public string DescribeAvailability(IConfiguration config, CkanModule m)
102
            => m.IsMetapackage
3✔
103
                ? string.Format(Properties.Resources.NetModuleCacheMetapackage, m.name, m.version)
104
                : IsMaybeCachedZip(m)
105
                    ? string.Format(Properties.Resources.NetModuleCacheModuleCached, m.name, m.version)
106
                    : DescribeUncachedAvailability(config, m, GetInProgressFileName(m));
107

108
        /// <summary>
109
        /// Calculate the SHA1 hash of a file
110
        /// </summary>
111
        /// <param name="filePath">Path to file to examine</param>
112
        /// <param name="progress">Callback to notify as we traverse the input, called with percentages from 0 to 100</param>
113
        /// <param name="cancelToken">Cancellation token to cancel the operation</param>
114
        /// <returns>
115
        /// SHA1 hash, in all-caps hexadecimal format
116
        /// </returns>
117
        public string GetFileHashSha1(string filePath, IProgress<int> progress, CancellationToken? cancelToken = default)
118
            => cache.GetFileHashSha1(filePath, progress, cancelToken);
3✔
119

120
        /// <summary>
121
        /// Calculate the SHA256 hash of a file
122
        /// </summary>
123
        /// <param name="filePath">Path to file to examine</param>
124
        /// <param name="progress">Callback to notify as we traverse the input, called with percentages from 0 to 100</param>
125
        /// <param name="cancelToken">Cancellation token to cancel the operation</param>
126
        /// <returns>
127
        /// SHA256 hash, in all-caps hexadecimal format
128
        /// </returns>
129
        public string GetFileHashSha256(string filePath, IProgress<int> progress, CancellationToken? cancelToken = default)
130
            => cache.GetFileHashSha256(filePath, progress, cancelToken);
3✔
131

132
        /// <summary>
133
        /// Try to add a file to the module cache.
134
        /// Throws exceptions if the file doesn't match the metadata.
135
        /// </summary>
136
        /// <param name="module">The module object corresponding to the download</param>
137
        /// <param name="path">Path to the file to add</param>
138
        /// <param name="progress">Callback to notify as we traverse the input, called with byte counts</param>
139
        /// <param name="description">Description of the file</param>
140
        /// <param name="move">True to move the file, false to copy</param>
141
        /// <param name="cancelToken">Cancellation token to cancel the operation</param>
142
        /// <param name="validate">True to validate the file, false to skip validation</param>
143
        /// <returns>
144
        /// Name of the new file in the cache
145
        /// </returns>
146
        public string Store(CkanModule         module,
147
                            string             path,
148
                            IProgress<long>?   progress,
149
                            string?            description = null,
150
                            bool               move        = false,
151
                            CancellationToken? cancelToken = default,
152
                            bool               validate    = true)
153
        {
154
            if (validate)
3✔
155
            {
156
                progress?.Report(0);
3✔
157
                // Check file exists
158
                FileInfo fi = new FileInfo(path);
3✔
159
                if (!fi.Exists)
3✔
160
                {
161
                    throw new FileNotFoundKraken(path);
3✔
162
                }
163

164
                // Check file size
165
                if (module.download_size > 0 && fi.Length != module.download_size)
3✔
166
                {
167
                    throw new InvalidModuleFileKraken(module, path, string.Format(
3✔
168
                        Properties.Resources.NetModuleCacheBadLength,
169
                        module, path, fi.Length, module.download_size));
170
                }
171

172
                cancelToken?.ThrowIfCancellationRequested();
3✔
173

174
                // Check valid CRC
175
                if (!ZipValid(path, out string invalidReason, progress, cancelToken))
3✔
176
                {
177
                    throw new InvalidModuleFileKraken(
3✔
178
                        module, path,
179
                        string.Format(Properties.Resources.NetModuleCacheNotValidZIP,
180
                                      module, path, invalidReason));
181
                }
182

183
                cancelToken?.ThrowIfCancellationRequested();
3✔
184
            }
185
            // If no exceptions, then everything is fine
186
            var success = //module.download is [Uri url, ..]
3✔
187
                          module.download != null
188
                          && module.download.Count > 0
189
                          && module.download[0] is Uri url
190
                            ? cache.Store(url, path,
191
                                          description ?? module.StandardName(),
192
                                          move)
193
                            : "";
194
            // Make sure completion is signalled so progress bars go away
195
            progress?.Report(new FileInfo(path).Length);
3✔
196
            ModStored?.Invoke(module);
3✔
197
            return success;
3✔
198
        }
199

200
        /// <summary>
201
        /// Check whether a ZIP file is valid
202
        /// </summary>
203
        /// <param name="filename">Path to zip file to check</param>
204
        /// <param name="invalidReason">Description of problem with the file</param>
205
        /// <param name="progress">Callback to notify as we traverse the input, called with byte counts</param>
206
        /// <param name="cancelToken">Cancellation token to cancel the operation</param>
207
        /// <returns>
208
        /// True if valid, false otherwise. See invalidReason param for explanation.
209
        /// </returns>
210
        public static bool ZipValid(string             filename,
211
                                    out string         invalidReason,
212
                                    IProgress<long>?   progress,
213
                                    CancellationToken? cancelToken = default)
214
        {
215
            try
216
            {
217
                if (filename != null)
3✔
218
                {
219
                    using (ZipFile zip = new ZipFile(filename))
3✔
220
                    {
221
                        string? zipErr = null;
3✔
222
                        // Limit progress updates to 100 per ZIP file
223
                        long totalBytesValidated = 0;
3✔
224
                        long previousBytesValidated = 0;
3✔
225
                        long onePercent = new FileInfo(filename).Length / 100;
3✔
226
                        // Perform CRC and other checks
227
                        if (zip.TestArchive(true, TestStrategy.FindFirstError,
3✔
228
                            (st, msg) =>
229
                            {
230
                                cancelToken?.ThrowIfCancellationRequested();
3✔
231
                                // This delegate is called as TestArchive proceeds through its
232
                                // steps, both routine and abnormal.
233
                                // The second parameter is non-null if an error occurred.
234
                                if (st != null)
3✔
235
                                {
236
                                    if (!st.EntryValid && !string.IsNullOrEmpty(msg))
3✔
237
                                    {
238
                                        // Capture the error string so we can return it
239
                                        zipErr = string.Format(
3✔
240
                                            Properties.Resources.NetFileCacheZipError,
241
                                            st.Operation, st.Entry?.Name, msg);
242
                                    }
243
                                    else if (st is { Operation: TestOperation.EntryComplete,
3✔
244
                                                     Entry:     ZipEntry entry }
245
                                             && progress != null)
246
                                    {
247
                                        // Report progress
248
                                        totalBytesValidated += entry.CompressedSize;
3✔
249
                                        if (totalBytesValidated - previousBytesValidated > onePercent)
3✔
250
                                        {
251
                                            progress.Report(totalBytesValidated);
3✔
252
                                            previousBytesValidated = totalBytesValidated;
3✔
253
                                        }
254
                                    }
255
                                }
256
                            }))
3✔
257
                        {
258
                            invalidReason = "";
3✔
259
                            return true;
3✔
260
                        }
261
                        else
262
                        {
263
                            invalidReason = zipErr ?? Properties.Resources.NetFileCacheZipTestArchiveFalse;
3✔
264
                            return false;
3✔
265
                        }
266
                    }
267
                }
268
                else
269
                {
270
                    invalidReason = Properties.Resources.NetFileCacheNullFileName;
×
271
                    return false;
×
272
                }
273
            }
274
            catch (ZipException ze)
3✔
275
            {
276
                // Save the errors someplace useful
277
                invalidReason = ze.Message;
3✔
278
                return false;
3✔
279
            }
280
            catch (ArgumentException ex)
×
281
            {
282
                invalidReason = ex.Message;
×
283
                return false;
×
284
            }
285
            catch (NotSupportedException nse) when (Platform.IsMono)
×
286
            {
287
                // SharpZipLib throws this if your locale isn't installed on Mono
288
                invalidReason = string.Format(Properties.Resources.NetFileCacheMonoNotSupported, nse.Message);
×
289
                return false;
×
290
            }
291
        }
3✔
292

293
        /// <summary>
294
        /// Remove a module's download files from the cache
295
        /// </summary>
296
        /// <param name="module">Module to purge</param>
297
        /// <returns>
298
        /// True if all purged, false otherwise
299
        /// </returns>
300
        public bool Purge(CkanModule module)
301
        {
302
            if (module.download != null
3✔
303
                && cache.Remove(module.download))
304
            {
305
                ModPurged?.Invoke(module);
3✔
306
                return true;
3✔
307
            }
308
            return false;
×
309
        }
310

311
        public bool Purge(IReadOnlyCollection<CkanModule> modules)
312
        {
313
            if (modules.Select(m => cache.Remove(m.download ?? Enumerable.Empty<Uri>()))
3✔
314
                       .ToArray()
315
                       .Any(removed => removed))
3✔
316
            {
317
                ModPurged?.Invoke(modules.First());
3✔
318
                return true;
3✔
319
            }
320
            return false;
×
321
        }
322

323
        public IReadOnlyDictionary<string, long> CachedFileSizeByHost(IReadOnlyDictionary<string, Uri> hashToURL)
324
            => cache.CachedHashesAndSizes()
3✔
325
                    // Skip downloads that changed URLs after downloading
326
                    .Where(tuple => hashToURL.ContainsKey(tuple.hash))
3✔
327
                    .GroupBy(tuple => hashToURL[tuple.hash].Host,
3✔
328
                             tuple => tuple.size)
3✔
329
                    .ToDictionary(grp => grp.Key,
3✔
330
                                  grp => grp.Sum());
3✔
331

332
        private readonly NetFileCache cache;
333
    }
334
}
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