diff --git a/ConsoleUI/DownloadImportDialog.cs b/ConsoleUI/DownloadImportDialog.cs index ff7fcb1537..c218360209 100644 --- a/ConsoleUI/DownloadImportDialog.cs +++ b/ConsoleUI/DownloadImportDialog.cs @@ -2,7 +2,6 @@ using System.IO; using System.Collections.Generic; using System.ComponentModel; -using System.Security.Cryptography; using CKAN.ConsoleUI.Toolkit; namespace CKAN.ConsoleUI { @@ -74,7 +73,7 @@ public static void ImportFiles(KSP gameInst, HashSet files, int percent = i * 100 / files.Count; user.RaiseProgress($"Importing {f.Name}... ({percent}%)", percent); // Calc SHA-1 sum - string sha1 = GetFileHashSha1(f.FullName); + string sha1 = NetModuleCache.GetFileHashSha1(f.FullName); // Find SHA-1 sum in registry (potentially multiple) if (index.ContainsKey(sha1)) { deletable.Add(f); @@ -83,11 +82,11 @@ public static void ImportFiles(KSP gameInst, HashSet files, if (mod.IsCompatibleKSP(gameInst.VersionCriteria())) { installable.Add(mod.identifier); } - if (inst.Cache.IsCachedZip(mod.download)) { + if (inst.Cache.IsMaybeCachedZip(mod)) { user.RaiseMessage("Already cached: {0}", f.Name); } else { user.RaiseMessage($"Importing {mod.identifier} {Formatting.StripEpoch(mod.version)}..."); - inst.Cache.Store(mod.download, f.FullName); + inst.Cache.Store(mod, f.FullName); } } } else { @@ -109,16 +108,6 @@ public static void ImportFiles(KSP gameInst, HashSet files, } } - private static string GetFileHashSha1(string filePath) - { - using (FileStream fs = new FileStream(filePath, FileMode.Open)) - using (BufferedStream bs = new BufferedStream(fs)) - using (SHA1Cng sha1 = new SHA1Cng()) - { - return BitConverter.ToString(sha1.ComputeHash(bs)).Replace("-", ""); - } - } - } } diff --git a/ConsoleUI/ModInfoScreen.cs b/ConsoleUI/ModInfoScreen.cs index 3be4cd81ed..8b0003f81a 100644 --- a/ConsoleUI/ModInfoScreen.cs +++ b/ConsoleUI/ModInfoScreen.cs @@ -103,7 +103,7 @@ public ModInfoScreen(KSPManager mgr, ChangePlan cp, CkanModule m, bool dbg) AddBinding(Keys.Escape, (object sender) => false); AddTip("Ctrl+D", "Download", - () => !manager.CurrentInstance.Cache.IsCachedZip(mod.download) + () => !manager.CurrentInstance.Cache.IsMaybeCachedZip(mod) ); AddBinding(Keys.CtrlD, (object sender) => { Download(); @@ -459,7 +459,7 @@ private void Download() () => { try { dl.DownloadModules(inst.Cache, new List {mod}); - if (!inst.Cache.IsCachedZip(mod.download)) { + if (!inst.Cache.IsMaybeCachedZip(mod)) { ps.RaiseError("Download failed, file is corrupted"); } } catch (Exception ex) { diff --git a/Core/CKAN-core.csproj b/Core/CKAN-core.csproj index 69f7596df7..0744042ae2 100644 --- a/Core/CKAN-core.csproj +++ b/Core/CKAN-core.csproj @@ -96,6 +96,7 @@ + diff --git a/Core/KSP.cs b/Core/KSP.cs index 0d7b7d5e73..9916be451c 100644 --- a/Core/KSP.cs +++ b/Core/KSP.cs @@ -36,7 +36,7 @@ public class KSP : IDisposable public KspVersion VersionOfKspWhenCompatibleVersionsWereStored { get; private set; } public bool CompatibleVersionsAreFromDifferentKsp { get { return _compatibleVersions.Count > 0 && VersionOfKspWhenCompatibleVersionsWereStored != Version(); } } - public NetFileCache Cache { get; private set; } + public NetModuleCache Cache { get; private set; } #endregion #region Construction and Initialisation @@ -56,7 +56,7 @@ public KSP(string gameDir, string name, IUser user) { SetupCkanDirectories(); LoadCompatibleVersions(); - Cache = new NetFileCache(DownloadCacheDir()); + Cache = new NetModuleCache(DownloadCacheDir()); } } diff --git a/Core/ModuleInstaller.cs b/Core/ModuleInstaller.cs index 7c13281451..c0848524c0 100644 --- a/Core/ModuleInstaller.cs +++ b/Core/ModuleInstaller.cs @@ -37,7 +37,7 @@ public class ModuleInstaller public ModuleInstallerReportModInstalled onReportModInstalled = null; // Our own cache is that of the KSP instance we're using. - public NetFileCache Cache + public NetModuleCache Cache { get { @@ -84,22 +84,22 @@ public static ModuleInstaller GetInstance(KSP ksp_instance, IUser user) /// /// Downloads the given mod to the cache. Returns the filename it was saved to. /// - public string Download(Uri url, string filename) + public string Download(CkanModule module, string filename) { - User.RaiseProgress(String.Format("Downloading \"{0}\"", url), 0); - return Download(url, filename, Cache); + User.RaiseProgress(String.Format("Downloading \"{0}\"", module.download), 0); + return Download(module, filename, Cache); } /// /// Downloads the given mod to the cache. Returns the filename it was saved to. /// - public static string Download(Uri url, string filename, NetFileCache cache) + public static string Download(CkanModule module, string filename, NetModuleCache cache) { log.Info("Downloading " + filename); - string tmp_file = Net.Download(url); + string tmp_file = Net.Download(module.download); - return cache.Store(url, tmp_file, filename, true); + return cache.Store(module, tmp_file, filename, true); } /// @@ -111,19 +111,7 @@ public static string Download(Uri url, string filename, NetFileCache cache) /// public string CachedOrDownload(CkanModule module, string filename = null) { - return CachedOrDownload(module.identifier, module.version, module.download, Cache, filename); - } - - /// - /// Returns the path to a cached copy of a module if it exists, or downloads - /// and returns the downloaded copy otherwise. - /// - /// If no filename is provided, the module's standard name will be used. - /// Chcecks the CKAN cache first. - /// - public string CachedOrDownload(string identifier, Version version, Uri url, string filename = null) - { - return CachedOrDownload(identifier, version, url, Cache, filename); + return CachedOrDownload(module, Cache, filename); } /// @@ -133,32 +121,27 @@ public string CachedOrDownload(string identifier, Version version, Uri url, stri /// If no filename is provided, the module's standard name will be used. /// Chcecks provided cache first. /// - public static string CachedOrDownload(string identifier, Version version, Uri url, NetFileCache cache, string filename = null) + public static string CachedOrDownload(CkanModule module, NetModuleCache cache, string filename = null) { if (filename == null) { - filename = CkanModule.StandardName(identifier, version); + filename = CkanModule.StandardName(module.identifier, module.version); } - string full_path = cache.GetCachedZip(url); + string full_path = cache.GetCachedZip(module); if (full_path == null) { - return Download(url, filename, cache); + return Download(module, filename, cache); } log.DebugFormat("Using {0} (cached)", filename); return full_path; } - public void InstallList( - List modules, - RelationshipResolverOptions options, - IDownloader downloader = null - ) + public void InstallList(List modules, RelationshipResolverOptions options, IDownloader downloader = null) { var resolver = new RelationshipResolver(modules, options, registry_manager.registry, ksp.VersionCriteria()); - var modsToInstall = resolver.ModList().ToList(); - InstallList(modsToInstall, options, downloader); + InstallList(resolver.ModList().ToList(), options, downloader); } /// @@ -169,14 +152,9 @@ public void InstallList( /// Propagates a FileExistsKraken if we were going to overwrite a file. /// Propagates a CancelledActionKraken if the user cancelled the install. /// - // - // TODO: Break this up into smaller pieces! It's huge! - public void InstallList( - ICollection modules, - RelationshipResolverOptions options, - IDownloader downloader = null - ) + public void InstallList(ICollection modules, RelationshipResolverOptions options, IDownloader downloader = null) { + // TODO: Break this up into smaller pieces! It's huge! var resolver = new RelationshipResolver(modules, options, registry_manager.registry, ksp.VersionCriteria()); var modsToInstall = resolver.ModList().ToList(); List downloads = new List(); @@ -188,7 +166,7 @@ public void InstallList( foreach (CkanModule module in modsToInstall) { - if (!ksp.Cache.IsCachedZip(module.download)) + if (!ksp.Cache.IsCachedZip(module)) { User.RaiseMessage(" * {0} {1} ({2})", module.name, module.version, module.download.Host); downloads.Add(module); @@ -287,7 +265,7 @@ public void InstallList(ModuleResolution modules, RelationshipResolverOptions op // TODO: Return files relative to GameRoot public IEnumerable GetModuleContentsList(CkanModule module) { - string filename = ksp.Cache.GetCachedZip(module.download); + string filename = ksp.Cache.GetCachedZip(module); if (filename == null) { @@ -327,7 +305,7 @@ private void Install(CkanModule module, string filename = null) } // Find our in the cache if we don't already have it. - filename = filename ?? Cache.GetCachedZip(module.download, true); + filename = filename ?? Cache.GetCachedZip(module); // If we *still* don't have a file, then kraken bitterly. if (filename == null) @@ -1099,7 +1077,7 @@ public void Upgrade(IEnumerable modules, IDownloader netAsyncDownloa /// private void DownloadModules(IEnumerable mods, IDownloader downloader) { - List downloads = mods.Where(module => !ksp.Cache.IsCachedZip(module.download)).ToList(); + List downloads = mods.Where(module => !ksp.Cache.IsCachedZip(module)).ToList(); if (downloads.Count > 0) { diff --git a/Core/Net/IDownloader.cs b/Core/Net/IDownloader.cs index ff2c671315..d595c95efc 100644 --- a/Core/Net/IDownloader.cs +++ b/Core/Net/IDownloader.cs @@ -11,11 +11,11 @@ public interface IDownloader /// Even if modules share download URLs, they will only be downloaded once. /// Blocks until the downloads are complete, cancelled, or errored. /// - void DownloadModules(NetFileCache cache, IEnumerable modules); + void DownloadModules(NetModuleCache cache, IEnumerable modules); /// /// Cancel any running downloads. /// void CancelDownload(); } -} \ No newline at end of file +} diff --git a/Core/Net/NetAsyncModulesDownloader.cs b/Core/Net/NetAsyncModulesDownloader.cs index 5ec9c84ac6..bd487e9bea 100644 --- a/Core/Net/NetAsyncModulesDownloader.cs +++ b/Core/Net/NetAsyncModulesDownloader.cs @@ -13,13 +13,13 @@ public class NetAsyncModulesDownloader : IDownloader { public IUser User { - get { return downloader.User; } + get { return downloader.User; } set { downloader.User = value; } } private static readonly ILog log = LogManager.GetLogger(typeof (NetAsyncModulesDownloader)); - private List modules; + private List modules; private readonly NetAsyncDownloader downloader; /// @@ -27,7 +27,7 @@ public IUser User /// public NetAsyncModulesDownloader(IUser user) { - modules = new List(); + modules = new List(); downloader = new NetAsyncDownloader(user); } @@ -35,10 +35,7 @@ public NetAsyncModulesDownloader(IUser user) /// /// /// - public void DownloadModules( - NetFileCache cache, - IEnumerable modules - ) + public void DownloadModules(NetModuleCache cache, IEnumerable modules) { // Walk through all our modules, but only keep the first of each // one that has a unique download path. @@ -67,7 +64,7 @@ IEnumerable modules /// Called by NetAsyncDownloader on completion. /// Called with all nulls on download cancellation. /// - private void ModuleDownloadsComplete(NetFileCache cache, Uri[] urls, string[] filenames, Exception[] errors) + private void ModuleDownloadsComplete(NetModuleCache cache, Uri[] urls, string[] filenames, Exception[] errors) { if (urls != null) { @@ -98,7 +95,7 @@ private void ModuleDownloadsComplete(NetFileCache cache, Uri[] urls, string[] fi try { - cache.Store(urls[i], filenames[i], modules[i].StandardName()); + cache.Store(modules[i], filenames[i], modules[i].StandardName()); } catch (FileNotFoundException e) { diff --git a/Core/Net/NetFileCache.cs b/Core/Net/NetFileCache.cs index c8479be05b..e305115289 100644 --- a/Core/Net/NetFileCache.cs +++ b/Core/Net/NetFileCache.cs @@ -182,36 +182,73 @@ public string GetCachedFilename(Uri url) /// when working with zip files. Returns null if not available, or /// validation failed. /// - /// Test data toggles if low level crc checks should be done. This can - /// take time on order of seconds for larger zip files. + /// Low level CRC (cyclic redundancy check) checks will be done. + /// This can take time on order of seconds for larger zip files. /// - public string GetCachedZip(Uri url, bool test_data = false) + public string GetCachedZip(Uri url) { string filename = GetCachedFilename(url); - - if (filename == null) + if (string.IsNullOrEmpty(filename)) { return null; } + else + { + string invalidReason; + if (ZipValid(filename, out invalidReason)) + { + return filename; + } + else + { + // Purge invalid cache entries + File.Delete(filename); + return null; + } + } + } + /// + /// Check whether a ZIP file is validation + /// + /// path to zip file to check + /// Description of problem with the file + /// + /// True if valid, false otherwise. See invalidReason param for explanation. + /// + public static bool ZipValid(string filename, out string invalidReason) + { try { - using (ZipFile zip = new ZipFile (filename)) + if (filename != null) { - // Perform CRC check. - if (zip.TestArchive(test_data)) + using (ZipFile zip = new ZipFile(filename)) { - return filename; + // Perform CRC check. + if (zip.TestArchive(true)) + { + invalidReason = ""; + return true; + } + else + { + invalidReason = "ZipFile.TestArchive(true) returned false"; + return false; + } } } + else + { + invalidReason = "Null file name"; + return false; + } } - catch (ZipException) + catch (ZipException ze) { - // We ignore these; it just means the file is borked, - // same as failing validation. + // Save the errors someplace useful + invalidReason = ze.Message; + return false; } - - return null; } /// diff --git a/Core/Net/NetModuleCache.cs b/Core/Net/NetModuleCache.cs new file mode 100644 index 0000000000..7b298f1300 --- /dev/null +++ b/Core/Net/NetModuleCache.cs @@ -0,0 +1,150 @@ +using System; +using System.IO; +using System.Security.Cryptography; + +namespace CKAN +{ + /// + /// A cache object that protects the validity of the files it contains. + /// A CkanModule must be provided for each file added, and the following + /// properties are checked before adding: + /// - CkanModule.download_size + /// - CkanModule.download_hash.sha1 + /// - CkanModule.download_hash.sha256 + /// + public class NetModuleCache : IDisposable + { + + /// + /// Initialize the cache + /// + /// Path to directory to use as the cache + public NetModuleCache(string path) + { + cache = new NetFileCache(path); + } + + // Simple passthrough wrappers + public void Dispose() + { + cache.Dispose(); + } + public void Clear() + { + cache.OnCacheChanged(); + } + public string GetCachePath() + { + return cache.GetCachePath(); + } + public bool IsCached(CkanModule m) + { + return cache.IsCached(m.download); + } + public bool IsCached(CkanModule m, out string outFilename) + { + return cache.IsCached(m.download, out outFilename); + } + public bool IsCachedZip(CkanModule m) + { + return cache.IsCachedZip(m.download); + } + public bool IsMaybeCachedZip(CkanModule m) + { + return cache.IsMaybeCachedZip(m.download); + } + public string GetCachedFilename(CkanModule m) + { + return cache.GetCachedFilename(m.download); + } + public string GetCachedZip(CkanModule m) + { + return cache.GetCachedZip(m.download); + } + + /// + /// Calculate the SHA1 hash of a file + /// + /// Path to file to examine + /// + /// SHA1 hash, in all-caps hexadecimal format + /// + public static string GetFileHashSha1(string filePath) + { + using (FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read)) + using (BufferedStream bs = new BufferedStream(fs)) + using (SHA1Cng sha1 = new SHA1Cng()) + { + return BitConverter.ToString(sha1.ComputeHash(bs)).Replace("-", ""); + } + } + + /// + /// Calculate the SHA256 hash of a file + /// + /// Path to file to examine + /// + /// SHA256 hash, in all-caps hexadecimal format + /// + public static string GetFileHashSha256(string filePath) + { + using (FileStream fs = new FileStream(@filePath, FileMode.Open, FileAccess.Read)) + using (BufferedStream bs = new BufferedStream(fs)) + using (SHA256Managed sha256 = new SHA256Managed()) + { + return BitConverter.ToString(sha256.ComputeHash(bs)).Replace("-", ""); + } + } + + /// + /// Try to add a file to the module cache. + /// Throws exceptions if the file doesn't match the metadata. + /// + /// The module object corresponding to the download + /// Path to the file to add + /// Description of the file + /// True to move the file, false to copy + /// + /// Name of the new file in the cache + /// + public string Store(CkanModule module, string path, string description = null, bool move = false) + { + // Check file exists + FileInfo fi = new FileInfo(path); + if (!fi.Exists) + throw new FileNotFoundKraken(path); + + // Check file size + if (fi.Length != module.download_size) + throw new InvalidModuleFileKraken(module, path, + $"{path} has length {fi.Length}, should be {module.download_size}"); + + // Check valid CRC + string invalidReason; + if (!NetFileCache.ZipValid(path, out invalidReason)) + throw new InvalidModuleFileKraken(module, path, + $"{path} is not a valid ZIP file: {invalidReason}"); + + // Some older metadata doesn't have hashes + if (module.download_hash != null) + { + // Check SHA1 match + string sha1 = GetFileHashSha1(path); + if (sha1 != module.download_hash.sha1) + throw new InvalidModuleFileKraken(module, path, + $"{path} has SHA1 {sha1}, should be {module.download_hash.sha1}"); + + // Check SHA256 match + string sha256 = GetFileHashSha256(path); + if (sha256 != module.download_hash.sha256) + throw new InvalidModuleFileKraken(module, path, + $"{path} has SHA256 {sha256}, should be {module.download_hash.sha256}"); + } + + // If no exceptions, then everything is fine + return cache.Store(module.download, path, description, move); + } + + private NetFileCache cache; + } +} diff --git a/Core/Types/Kraken.cs b/Core/Types/Kraken.cs index b2adef565a..17f92499ff 100644 --- a/Core/Types/Kraken.cs +++ b/Core/Types/Kraken.cs @@ -340,4 +340,39 @@ public override string ToString() return String.Format("CKAN is already running for this instance!\n\nIf you're certain this is not the case, then delete:\n\"{0}\"\n", lockfilePath); } } + + /// + /// Exception thrown when a downloaded file isn't valid for a module. + /// Happens if: + /// 1. Size doesn't match download_size + /// 2. Not a valid ZIP file + /// 3. SHA1 doesn't match download_hash.sha1 + /// 4. SHA256 doesn't match download_hash.sha256 + /// + public class InvalidModuleFileKraken : Kraken + { + /// + /// The module that doesn't match the file + /// + public readonly CkanModule module; + + /// + /// Path to the file that doesn't match the module + /// + public readonly string path; + + /// + /// Release the kraken + /// + /// Module to check against path + /// Path to the file to check against module + /// Human-readable description of the problem + public InvalidModuleFileKraken(CkanModule module, string path, string reason = null) + : base(reason) + { + this.module = module; + this.path = path; + } + } + } diff --git a/GUI/GUIMod.cs b/GUI/GUIMod.cs index a2bf16eeca..6b3ebbeab5 100644 --- a/GUI/GUIMod.cs +++ b/GUI/GUIMod.cs @@ -193,7 +193,7 @@ public void UpdateIsCached() return; } - IsCached = Main.Instance.CurrentInstance.Cache.IsMaybeCachedZip(Mod.download); + IsCached = Main.Instance.CurrentInstance.Cache.IsMaybeCachedZip(Mod); } public CkanModule ToCkanModule() diff --git a/GUI/SettingsDialog.cs b/GUI/SettingsDialog.cs index 95e813b818..60d958bade 100644 --- a/GUI/SettingsDialog.cs +++ b/GUI/SettingsDialog.cs @@ -106,7 +106,7 @@ private void ClearCKANCacheButton_Click(object sender, EventArgs e) } // tell the cache object to nuke itself - Main.Instance.CurrentInstance.Cache.OnCacheChanged(); + Main.Instance.CurrentInstance.Cache.Clear(); // forcibly tell all mod rows to re-check cache state foreach (DataGridViewRow row in Main.Instance.ModList.Rows) diff --git a/Netkan/Program.cs b/Netkan/Program.cs index e55ed4fd80..32ddecc32e 100644 --- a/Netkan/Program.cs +++ b/Netkan/Program.cs @@ -133,7 +133,8 @@ private static NetFileCache FindCache(KSPManager kspManager) { var ksp = kspManager.GetPreferredInstance(); Log.InfoFormat("Using CKAN cache at {0}", ksp.Cache.GetCachePath()); - return ksp.Cache; + /// Create a new file cache in the same location so NetKAN can download pure URLs not sourced from CkanModules + return new NetFileCache(ksp.Cache.GetCachePath()); } catch { diff --git a/Netkan/Services/FileService.cs b/Netkan/Services/FileService.cs index 0d1b67af97..8438ab1902 100644 --- a/Netkan/Services/FileService.cs +++ b/Netkan/Services/FileService.cs @@ -13,35 +13,25 @@ public long GetSizeBytes(string filePath) public string GetFileHashSha1(string filePath) { - using (FileStream fs = new FileStream(@filePath, FileMode.Open)) - using (BufferedStream bs = new BufferedStream(fs)) - using (var sha1 = new SHA1Cng()) - { - byte[] hash = sha1.ComputeHash(bs); - - return BitConverter.ToString(hash).Replace("-", ""); - } + // Use shared implementation from Core. + // Also needs to be an instance method so it can be Moq'd for testing. + return NetModuleCache.GetFileHashSha1(filePath); } public string GetFileHashSha256(string filePath) { - using (FileStream fs = new FileStream(@filePath, FileMode.Open)) - using (BufferedStream bs = new BufferedStream(fs)) - using (var sha256 = new SHA256Managed()) - { - byte[] hash = sha256.ComputeHash(bs); - - return BitConverter.ToString(hash).Replace("-", ""); - } + // Use shared implementation from Core. + // Also needs to be an instance method so it can be Moq'd for testing. + return NetModuleCache.GetFileHashSha256(filePath); } - + public string GetMimetype(string filePath) { string mimetype; switch (FileIdentifier.IdentifyFile(filePath)) { - case FileType.ASCII: + case FileType.ASCII: mimetype = "text/plain"; break; case FileType.GZip: @@ -60,7 +50,7 @@ public string GetMimetype(string filePath) mimetype = "application/octet-stream"; break; } - + return mimetype; } } diff --git a/Netkan/Sources/Spacedock/SDVersion.cs b/Netkan/Sources/Spacedock/SDVersion.cs index cdab7f18c9..4ef58926bf 100644 --- a/Netkan/Sources/Spacedock/SDVersion.cs +++ b/Netkan/Sources/Spacedock/SDVersion.cs @@ -24,17 +24,6 @@ public class SDVersion public Version friendly_version; public int id; - public string Download(string identifier, NetFileCache cache) - { - log.DebugFormat("Downloading {0}", download_path); - - string filename = ModuleInstaller.CachedOrDownload(identifier, friendly_version, download_path, cache); - - log.Debug("Downloaded."); - - return filename; - } - /// /// SpaceDock always trims trailing zeros from a three-part version /// (eg: 1.0.0 -> 1.0). This means we could potentially think some mods @@ -106,4 +95,4 @@ public override bool CanConvert(Type objectType) } } } -} \ No newline at end of file +} diff --git a/Tests/Core/Cache.cs b/Tests/Core/Cache.cs index b14b36c81c..7fce3e2241 100644 --- a/Tests/Core/Cache.cs +++ b/Tests/Core/Cache.cs @@ -11,14 +11,16 @@ public class Cache { private string cache_dir; - private NetFileCache cache; + private NetFileCache cache; + private NetModuleCache module_cache; [SetUp] public void MakeCache() { cache_dir = TestData.NewTempDir(); Directory.CreateDirectory(cache_dir); - cache = new NetFileCache(cache_dir); + cache = new NetFileCache(cache_dir); + module_cache = new NetModuleCache(cache_dir); } [TearDown] @@ -96,10 +98,42 @@ public void CacheKraken() } catch (DirectoryNotFoundKraken kraken) { - Assert.AreSame(dir,kraken.directory); + Assert.AreSame(dir, kraken.directory); } } + [Test] + public void StoreInvalid() + { + // Try to store a nonexistent zip into a NetModuleCache + // and expect an FileNotFoundKraken + Assert.Throws(() => + module_cache.Store( + TestData.DogeCoinFlag_101_LZMA_module, + "/DoesNotExist.zip" + ) + ); + + // Try to store the LZMA-format DogeCoin zip into a NetModuleCache + // and expect an InvalidModuleFileKraken + Assert.Throws(() => + module_cache.Store( + TestData.DogeCoinFlag_101_LZMA_module, + TestData.DogeCoinFlagZipLZMA + ) + ); + + // Try to store the normal DogeCoin zip into a NetModuleCache + // using the WRONG metadata (file size and hashes) + // and expect an InvalidModuleFileKraken + Assert.Throws(() => + module_cache.Store( + TestData.DogeCoinFlag_101_LZMA_module, + TestData.DogeCoinFlagZip() + ) + ); + } + [Test] public void DoubleCache() { @@ -146,4 +180,3 @@ public void ZipValidation() } } } - diff --git a/Tests/Core/ModuleInstaller.cs b/Tests/Core/ModuleInstaller.cs index 64420cd8fb..848330eb01 100644 --- a/Tests/Core/ModuleInstaller.cs +++ b/Tests/Core/ModuleInstaller.cs @@ -418,11 +418,11 @@ public void CanInstallMod() Assert.IsFalse(File.Exists(mod_file_path)); // Copy the zip file to the cache directory. - Assert.IsFalse(ksp.KSP.Cache.IsCachedZip(TestData.DogeCoinFlag_101_module().download)); + Assert.IsFalse(ksp.KSP.Cache.IsCachedZip(TestData.DogeCoinFlag_101_module())); - string cache_path = ksp.KSP.Cache.Store(TestData.DogeCoinFlag_101_module().download, TestData.DogeCoinFlagZip()); + string cache_path = ksp.KSP.Cache.Store(TestData.DogeCoinFlag_101_module(), TestData.DogeCoinFlagZip()); - Assert.IsTrue(ksp.KSP.Cache.IsCachedZip(TestData.DogeCoinFlag_101_module().download)); + Assert.IsTrue(ksp.KSP.Cache.IsCachedZip(TestData.DogeCoinFlag_101_module())); Assert.IsTrue(File.Exists(cache_path)); // Mark it as available in the registry. @@ -459,7 +459,7 @@ public void CanUninstallMod() // Install the test mod. var registry = CKAN.RegistryManager.Instance(ksp.KSP).registry; - ksp.KSP.Cache.Store(TestData.DogeCoinFlag_101_module().download, TestData.DogeCoinFlagZip()); + ksp.KSP.Cache.Store(TestData.DogeCoinFlag_101_module(), TestData.DogeCoinFlagZip()); registry.AddAvailable(TestData.DogeCoinFlag_101_module()); List modules = new List { TestData.DogeCoinFlag_101_module().identifier }; @@ -494,7 +494,7 @@ public void UninstallEmptyDirs() // Install the base test mod. var registry = CKAN.RegistryManager.Instance(ksp.KSP).registry; - ksp.KSP.Cache.Store(TestData.DogeCoinFlag_101_module().download, TestData.DogeCoinFlagZip()); + ksp.KSP.Cache.Store(TestData.DogeCoinFlag_101_module(), TestData.DogeCoinFlagZip()); registry.AddAvailable(TestData.DogeCoinFlag_101_module()); List modules = new List { TestData.DogeCoinFlag_101_module().identifier }; @@ -504,7 +504,7 @@ public void UninstallEmptyDirs() modules.Clear(); // Install the plugin test mod. - ksp.KSP.Cache.Store(TestData.DogeCoinPlugin_module().download, TestData.DogeCoinPluginZip()); + ksp.KSP.Cache.Store(TestData.DogeCoinPlugin_module(), TestData.DogeCoinPluginZip()); registry.AddAvailable(TestData.DogeCoinPlugin_module()); modules.Add(TestData.DogeCoinPlugin_module().identifier); @@ -541,7 +541,7 @@ public void ModuleManagerInstancesAreDecoupled() using (DisposableKSP ksp = new DisposableKSP()) { // Copy the zip file to the cache directory. - ksp.KSP.Cache.Store(TestData.DogeCoinFlag_101_module().download, TestData.DogeCoinFlagZip()); + ksp.KSP.Cache.Store(TestData.DogeCoinFlag_101_module(), TestData.DogeCoinFlagZip()); // Mark it as available in the registry. var registry = CKAN.RegistryManager.Instance(ksp.KSP).registry; @@ -655,4 +655,4 @@ private static void TestDogeCoinStanza(ModuleInstallDescriptor stanza) } } -} \ No newline at end of file +} diff --git a/Tests/Core/ModuleInstallerDirTest.cs b/Tests/Core/ModuleInstallerDirTest.cs index 421860cfb6..c6d6601807 100644 --- a/Tests/Core/ModuleInstallerDirTest.cs +++ b/Tests/Core/ModuleInstallerDirTest.cs @@ -17,11 +17,11 @@ namespace Tests.Core [TestFixture] public class ModuleInstallerDirTest { - private DisposableKSP _instance; - private CKAN.Registry _registry; + private DisposableKSP _instance; + private CKAN.Registry _registry; private CKAN.ModuleInstaller _installer; - private CKAN.CkanModule _testModule; - private string _gameDataDir; + private CKAN.CkanModule _testModule; + private string _gameDataDir; /// /// Prep environment by setting up a single mod in @@ -32,14 +32,14 @@ public void SetUp() { _testModule = TestData.DogeCoinFlag_101_module(); - _instance = new DisposableKSP(); - _registry = CKAN.RegistryManager.Instance(_instance.KSP).registry; + _instance = new DisposableKSP(); + _registry = CKAN.RegistryManager.Instance(_instance.KSP).registry; _installer = CKAN.ModuleInstaller.GetInstance(_instance.KSP, NullUser.User); _gameDataDir = _instance.KSP.GameData(); _registry.AddAvailable(_testModule); var testModFile = TestData.DogeCoinFlagZip(); - _instance.KSP.Cache.Store(_testModule.download, testModFile); + _instance.KSP.Cache.Store(_testModule, testModFile); _installer.InstallList( new List() { _testModule.identifier }, new RelationshipResolverOptions() @@ -116,7 +116,7 @@ public void TestCaseSensitivity() { var size = _installer.AddParentDirectories(paths).Count; // each directory adds two directories to the result - // two directorie sets { GAMEDATA and gamedata } = 4 objects in result array + // two directories each set { GAMEDATA and gamedata } = 4 objects in result array if (size != 4) { throw new InvalidOperationException("Directories have case-sensitive differences"); diff --git a/Tests/Core/Net/NetAsyncModulesDownloader.cs b/Tests/Core/Net/NetAsyncModulesDownloader.cs index 876ac9580d..6e7d734917 100644 --- a/Tests/Core/Net/NetAsyncModulesDownloader.cs +++ b/Tests/Core/Net/NetAsyncModulesDownloader.cs @@ -18,7 +18,7 @@ public class NetAsyncModulesDownloader private CKAN.Registry registry; private DisposableKSP ksp; private CKAN.IDownloader async; - private NetFileCache cache; + private NetModuleCache cache; private static readonly ILog log = LogManager.GetLogger(typeof (NetAsyncModulesDownloader)); @@ -69,7 +69,7 @@ public void SingleDownload() modules.Add(kOS); // Make sure we don't alread have kOS somehow. - Assert.IsFalse(cache.IsCached(kOS.download)); + Assert.IsFalse(cache.IsCached(kOS)); // log.InfoFormat("Downloading kOS from {0}",kOS.download); @@ -81,7 +81,7 @@ public void SingleDownload() ); // Assert that we have it, and it passes zip validation. - Assert.IsTrue(cache.IsCachedZip(kOS.download)); + Assert.IsTrue(cache.IsCachedZip(kOS)); } [Test] @@ -98,13 +98,13 @@ public void MultiDownload() modules.Add(kOS); modules.Add(quick_revert); - Assert.IsFalse(cache.IsCachedZip(kOS.download)); - Assert.IsFalse(cache.IsCachedZip(quick_revert.download)); + Assert.IsFalse(cache.IsCachedZip(kOS)); + Assert.IsFalse(cache.IsCachedZip(quick_revert)); async.DownloadModules(cache, modules); - Assert.IsTrue(cache.IsCachedZip(kOS.download)); - Assert.IsTrue(cache.IsCachedZip(quick_revert.download)); + Assert.IsTrue(cache.IsCachedZip(kOS)); + Assert.IsTrue(cache.IsCachedZip(quick_revert)); } [Test] @@ -119,11 +119,11 @@ public void RandSdownload() modules.Add(rAndS); - Assert.IsFalse(cache.IsCachedZip(rAndS.download), "Module not yet downloaded"); + Assert.IsFalse(cache.IsCachedZip(rAndS), "Module not yet downloaded"); async.DownloadModules(cache, modules); - Assert.IsTrue(cache.IsCachedZip(rAndS.download),"Module download successful"); + Assert.IsTrue(cache.IsCachedZip(rAndS),"Module download successful"); } } diff --git a/Tests/Data/DogeCoinFlag-1.01-LZMA.zip b/Tests/Data/DogeCoinFlag-1.01-LZMA.zip new file mode 100644 index 0000000000..2bb92cddb1 Binary files /dev/null and b/Tests/Data/DogeCoinFlag-1.01-LZMA.zip differ diff --git a/Tests/Data/TestData.cs b/Tests/Data/TestData.cs index c7916008ed..7f3ec702a7 100644 --- a/Tests/Data/TestData.cs +++ b/Tests/Data/TestData.cs @@ -71,6 +71,54 @@ public static string DogeCoinPluginZip() return Path.Combine(DataDir(), "DogeCoinPlugin.zip"); } + /// + /// DogeCoinFlag 1.01 info. This doesn't contain any bugs. + /// + public static string DogeCoinFlag_101_LZMA() + { + return @" + { + ""spec_version"": 1, + ""identifier"": ""DogeCoinFlag"", + ""install"": [ + { + ""file"": ""DogeCoinFlag-1.01/GameData/DogeCoinFlag"", + ""install_to"": ""GameData"", + ""filter"" : [ ""Thumbs.db"", ""README.md"" ], + ""filter_regexp"" : ""\\.bak$"" + } + ], + ""resources"": { + ""kerbalstuff"": { + ""url"": ""https://kerbalstuff.com/mod/269/Dogecoin%20Flag"" + }, + ""homepage"": ""https://www.reddit.com/r/dogecoin/comments/1tdlgg/i_made_a_more_accurate_dogecoin_and_a_ksp_flag/"" + }, + ""name"": ""Dogecoin Flag"", + ""license"": ""CC-BY"", + ""abstract"": ""Such flag. Very currency. To the mun! Wow!"", + ""author"": ""pjf"", + ""version"": ""1.01"", + ""download"": ""https://kerbalstuff.com/mod/269/Dogecoin%20Flag/download/1.01"", + ""comment"": ""Generated by ks2ckan"", + ""download_size"": 54178, + ""download_hash"": { + ""sha1"": ""47B6ED5F502AD914744882858345BE030A29E1AA"", + ""sha256"": ""EC955DB772FBA8CAA62BF61C180D624C350D792C6F573D35A5EAEE3898DCF7C1"" + }, + ""ksp_version"": ""0.25"" + } + "; + } + + public static CkanModule DogeCoinFlag_101_LZMA_module = CkanModule.FromJson( + DogeCoinFlag_101_LZMA() + ); + + /// + /// Test case for LZMA-format ZIPs + /// + public static string DogeCoinFlagZipLZMA = Path.Combine(DataDir(), "DogeCoinFlag-1.01-LZMA.zip"); /// /// DogeCoinFlag 1.01 info. This contains a bug where the @@ -84,13 +132,13 @@ public static string DogeCoinFlag_101_bugged() ""identifier"": ""DogeCoinFlag"", ""install"": [ { - ""file"": ""GameData/DogeCoinFlag"", - ""install_to"": ""GameData"" + ""file"": ""GameData/DogeCoinFlag"", + ""install_to"": ""GameData"" } ], ""resources"": { ""kerbalstuff"": { - ""url"": ""https://kerbalstuff.com/mod/269/Dogecoin%20Flag"" + ""url"": ""https://kerbalstuff.com/mod/269/Dogecoin%20Flag"" }, ""homepage"": ""https://www.reddit.com/r/dogecoin/comments/1tdlgg/i_made_a_more_accurate_dogecoin_and_a_ksp_flag/"" }, @@ -102,6 +150,10 @@ public static string DogeCoinFlag_101_bugged() ""download"": ""https://kerbalstuff.com/mod/269/Dogecoin%20Flag/download/1.01"", ""comment"": ""Generated by ks2ckan"", ""download_size"": 53647, + ""download_hash"": { + ""sha1"": ""47B6ED5F502AD914744882858345BE030A29E1AA"", + ""sha256"": ""EC955DB772FBA8CAA62BF61C180D624C350D792C6F573D35A5EAEE3898DCF7C1"" + }, ""ksp_version"": ""0.25"" } "; @@ -143,6 +195,10 @@ public static string DogeCoinFlag_101() ""download"": ""https://kerbalstuff.com/mod/269/Dogecoin%20Flag/download/1.01"", ""comment"": ""Generated by ks2ckan"", ""download_size"": 53647, + ""download_hash"": { + ""sha1"": ""47B6ED5F502AD914744882858345BE030A29E1AA"", + ""sha256"": ""EC955DB772FBA8CAA62BF61C180D624C350D792C6F573D35A5EAEE3898DCF7C1"" + }, ""ksp_version"": ""0.25"" } "; @@ -212,6 +268,10 @@ public static string FutureMetaData() ""download"": ""https://kerbalstuff.com/mod/269/Dogecoin%20Flag/download/1.01"", ""comment"": ""Generated by ks2ckan"", ""download_size"": 53647, + ""download_hash"": { + ""sha1"": ""47B6ED5F502AD914744882858345BE030A29E1AA"", + ""sha256"": ""EC955DB772FBA8CAA62BF61C180D624C350D792C6F573D35A5EAEE3898DCF7C1"" + }, ""ksp_version"": ""0.25"" } "; @@ -246,7 +306,11 @@ public static string DogeCoinPlugin() ""author"": ""politas"", ""version"": ""1.01"", ""download"": ""https://kerbalstuff.com/mod/269/Dogecoin%20Flag/download/1.01"", - ""download_size"": 53647, + ""download_size"": 528, + ""download_hash"": { + ""sha1"": ""8B4F4DD53E702E0C1D3FBCA1C9CB4E4027F50E8A"", + ""sha256"": ""CEB1F30B8E1561EFD7F02D680407FE8FEC36B37AE073ABA8B06EEE05701F0EEF"", + }, ""ksp_version"": ""0.25"" } "; @@ -382,6 +446,10 @@ public static string kOS_014_with_invalid_version_characters() ""identifier"" : ""kOS"", ""abstract"" : ""A programming and automation environment for KSP craft."", ""download"" : ""https://github.com/KSP-KOS/KOS/releases/download/v0.14/kOS.v14.zip"", + ""download_hash"": { + ""sha1"": ""C5A224AC4397770C0B19B4A6417F6C5052191608"", + ""sha256"": ""E0FB79C81D8FCDA8DB6E38B104106C3B7D078FDC06ACA2BC7834973B43D789CB"" + }, ""license"" : ""GPL-3.0"", ""version"" : ""0:14^0"", ""release_status"" : ""stable"", @@ -415,6 +483,10 @@ public static string kOS_014_multilicense() ""identifier"" : ""kOS"", ""abstract"" : ""A programming and automation environment for KSP craft."", ""download"" : ""https://github.com/KSP-KOS/KOS/releases/download/v0.14/kOS.v14.zip"", + ""download_hash"": { + ""sha1"": ""C5A224AC4397770C0B19B4A6417F6C5052191608"", + ""sha256"": ""E0FB79C81D8FCDA8DB6E38B104106C3B7D078FDC06ACA2BC7834973B43D789CB"" + }, ""license"" : [ ""GPL-3.0"", ""GPL-2.0"" ], ""version"" : ""0.14"", ""release_status"" : ""stable"", @@ -570,4 +642,3 @@ public CkanModule GeneratorRandomModule( } } } - diff --git a/Tests/GUI/GH1866.cs b/Tests/GUI/GH1866.cs index 26ebd12dd7..ddc015908f 100644 --- a/Tests/GUI/GH1866.cs +++ b/Tests/GUI/GH1866.cs @@ -28,24 +28,26 @@ public class GH1866 /* * an exception would be thrown at the bottom of this */ - /*var main = new Main(null, new GUIUser(), false); - main.Manager = _manager; - // first sort by name - main.configuration.SortByColumnIndex = 2; - // now sort by version - main.configuration.SortByColumnIndex = 6; - main.MarkModForInstall("kOS"); - - // make sure we have one requested change - var changeList = main.mainModList.ComputeUserChangeSet() - .Select((change) => change.Mod.ToCkanModule()).ToList(); - - // do the install - ModuleInstaller.GetInstance(_instance.KSP, main.currentUser).InstallList( - changeList, - new RelationshipResolverOptions(), - new NetAsyncModulesDownloader(main.currentUser) - );*/ + /* + var main = new Main(null, new GUIUser(), false); + main.Manager = _manager; + // first sort by name + main.configuration.SortByColumnIndex = 2; + // now sort by version + main.configuration.SortByColumnIndex = 6; + main.MarkModForInstall("kOS"); + + // make sure we have one requested change + var changeList = main.mainModList.ComputeUserChangeSet() + .Select((change) => change.Mod.ToCkanModule()).ToList(); + + // do the install + ModuleInstaller.GetInstance(_instance.KSP, main.currentUser).InstallList( + changeList, + new RelationshipResolverOptions(), + new NetAsyncModulesDownloader(main.currentUser) + ); + */ [OneTimeSetUp] public void Up() @@ -58,7 +60,7 @@ public void Up() _anyVersionModule = TestData.DogeCoinFlag_101_module(); // install it and set it as pre-installed - _instance.KSP.Cache.Store(TestData.DogeCoinFlag_101_module().download, TestData.DogeCoinFlagZip()); + _instance.KSP.Cache.Store(TestData.DogeCoinFlag_101_module(), TestData.DogeCoinFlagZip()); _registry.RegisterModule(_anyVersionModule, new string[] { }, _instance.KSP); _registry.AddAvailable(_anyVersionModule); diff --git a/Tests/NetKAN/Validators/CkanValidatorTests.cs b/Tests/NetKAN/Validators/CkanValidatorTests.cs index 8dd8413cfc..ad3fb4b37d 100644 --- a/Tests/NetKAN/Validators/CkanValidatorTests.cs +++ b/Tests/NetKAN/Validators/CkanValidatorTests.cs @@ -52,7 +52,7 @@ public void DoesNotThrowOnValidCkan() [TestCase("identifier")] [TestCase("version")] [TestCase("download")] - public void DoesThrowWhenMissingProeprty(string propertyName) + public void DoesThrowWhenMissingProperty(string propertyName) { // Arrange var mHttp = new Mock();