public void GenerateDefaultInstall() { string filename = Tests.TestData.DogeCoinFlagZip(); using (var zipfile = new ZipFile(filename)) { CKAN.ModuleInstallDescriptor stanza = CKAN.ModuleInstaller.GenerateDefaultInstall("DogeCoinFlag", zipfile); TestDogeCoinStanza(stanza); // Same again, but screwing up the case (we see this *all the time*) CKAN.ModuleInstallDescriptor stanza2 = CKAN.ModuleInstaller.GenerateDefaultInstall("DogecoinFlag", zipfile); TestDogeCoinStanza(stanza2); // Now what happens if we can't find what to install? Assert.Throws <FileNotFoundKraken>(delegate { CKAN.ModuleInstaller.GenerateDefaultInstall("Xyzzy", zipfile); }); // Make sure the FNFKraken looks like what we expect. try { CKAN.ModuleInstaller.GenerateDefaultInstall("Xyzzy", zipfile); } catch (FileNotFoundKraken kraken) { Assert.AreEqual("Xyzzy", kraken.file); } } }
/// <summary> /// Given a module and an open zipfile, return all the files that would be installed /// for this module. /// /// If a KSP instance is provided, it will be used to generate output paths, otherwise these will be null. /// /// Throws a BadMetadataKraken if the stanza resulted in no files being returned. /// </summary> public static List <InstallableFile> FindInstallableFiles(CkanModule module, ZipFile zipfile, KSP ksp) { var files = new List <InstallableFile> (); try { // Use the provided stanzas, or use the default install stanza if they're absent. if (module.install != null && module.install.Length != 0) { foreach (ModuleInstallDescriptor stanza in module.install) { files.AddRange(FindInstallableFiles(stanza, zipfile, ksp)); } } else { ModuleInstallDescriptor default_stanza = GenerateDefaultInstall(module.identifier, zipfile); files.AddRange(FindInstallableFiles(default_stanza, zipfile, ksp)); } } catch (BadMetadataKraken kraken) { // Decorate our kraken with the current module, as the lower-level // methods won't know it. kraken.module = module; throw; } return(files); }
/// <summary> /// Given a zipfile, returns a `file` ModuleInstallDescriptor that can be used for /// installation. /// Returns `this` if already of a `file` type. /// </summary> /// <param name="zipfile">Downloaded ZIP file containing the mod</param> public ModuleInstallDescriptor ConvertFindToFile(ZipFile zipfile) { // If we're already a file type stanza, then we have nothing to do. if (this.file != null) { return(this); } // Match *only* things with our find string as a directory. // We can't just look for directories, because some zipfiles // don't include entries for directories, but still include entries // for the files they contain. Regex inst_filt = this.find != null ? new Regex(@"(?:^|/)" + Regex.Escape(this.find) + @"$", RegexOptions.IgnoreCase) : new Regex(this.find_regexp, RegexOptions.IgnoreCase); // Find the shortest directory path that matches our filter, // including all parent directories of all entries. string shortest = null; foreach (ZipEntry entry in zipfile) { bool is_file = !entry.IsDirectory; // Normalize path before searching (path separator as '/', no trailing separator) for (string path = Regex.Replace(entry.Name.Replace('\\', '/'), "/$", ""); !string.IsNullOrEmpty(path); path = Path.GetDirectoryName(path).Replace('\\', '/'), is_file = false) { // Skip file paths if not allowed if (!find_matches_files && is_file) { continue; } // Is this a shorter matching path? if ((string.IsNullOrEmpty(shortest) || path.Length < shortest.Length) && inst_filt.IsMatch(path)) { shortest = path; } } } if (string.IsNullOrEmpty(shortest)) { throw new FileNotFoundKraken( this.find ?? this.find_regexp, String.Format("Could not find {0} entry in zipfile to install", this.find ?? this.find_regexp) ); } // Fill in our stanza, and remove our old `find` and `find_regexp` info. ModuleInstallDescriptor stanza = (ModuleInstallDescriptor)this.Clone(); stanza.file = shortest; stanza.find = null; stanza.find_regexp = null; return(stanza); }
/// <summary> /// Returns a default install stanza for the identifer provided. /// </summary> public static ModuleInstallDescriptor DefaultInstallStanza(string ident, ZipFile zipfile) { // Really this is just making a dummy `find` stanza and returning the processed // result. var stanza = new ModuleInstallDescriptor(); stanza.install_to = "GameData"; stanza.find = ident; return stanza.ConvertFindToFile(zipfile); }
private static bool InstallStanzaEquals(ModuleInstallDescriptor newInst, ModuleInstallDescriptor oldInst) { if (newInst.file != oldInst.file) { return(false); } if (newInst.install_to != oldInst.install_to) { return(false); } if (newInst.@as != oldInst.@as) { return(false); } if ((newInst.filter == null) != (oldInst.filter == null)) { return(false); } if (newInst.filter != null && !newInst.filter.SequenceEqual(oldInst.filter)) { return(false); } if ((newInst.filter_regexp == null) != (oldInst.filter_regexp == null)) { return(false); } if (newInst.filter_regexp != null && !newInst.filter_regexp.SequenceEqual(oldInst.filter_regexp)) { return(false); } if (newInst.find_matches_files != oldInst.find_matches_files) { return(false); } if ((newInst.include_only == null) != (oldInst.include_only == null)) { return(false); } if (newInst.include_only != null && !newInst.include_only.SequenceEqual(oldInst.include_only)) { return(false); } if ((newInst.include_only_regexp == null) != (oldInst.include_only_regexp == null)) { return(false); } if (newInst.include_only_regexp != null && !newInst.include_only_regexp.SequenceEqual(oldInst.include_only_regexp)) { return(false); } return(true); }
/// <summary> /// Returns a default install stanza for the identifer provided. /// </summary> public static ModuleInstallDescriptor DefaultInstallStanza(string ident, ZipFile zipfile) { // Really this is just making a dummy `find` stanza and returning the processed // result. var stanza = new ModuleInstallDescriptor(); stanza.install_to = "GameData"; stanza.find = ident; return(stanza.ConvertFindToFile(zipfile)); }
/// <summary> /// Returns a default install stanza for the module provided. This finds the topmost /// directory which matches the module identifier, and generates a stanza that /// installs that into GameData. /// /// Throws a FileNotFoundKraken() if unable to locate a suitable directory. /// </summary> internal static ModuleInstallDescriptor GenerateDefaultInstall(string identifier, ZipFile zipfile) { var stanza = new ModuleInstallDescriptor(); stanza.install_to = "GameData"; // Candidate top-level directories. var candidate_set = new HashSet <string>(); // Match *only* things with our module identifier as a directory. // We can't just look for directories, because some zipfiles // don't include entries for directories, but still include entries // for the files they contain. string ident_filter = @"(?:^|/)" + Regex.Escape(identifier) + @"$"; // Let's find that directory foreach (ZipEntry entry in zipfile) { string directory = Path.GetDirectoryName(entry.Name); // Normalise our path. directory = directory.Replace('\\', '/'); directory = Regex.Replace(directory, "/$", ""); // If this looks like what we're after, remember it. if (Regex.IsMatch(directory, ident_filter, RegexOptions.IgnoreCase)) { candidate_set.Add(directory); } } // Sort to have shortest first. It's not *quite* top-level directory order, // but it's good enough for now. var candidates = new List <string>(candidate_set); candidates.Sort((a, b) => a.Length.CompareTo(b.Length)); if (candidates.Count == 0) { throw new FileNotFoundKraken( identifier, String.Format("Could not find {0} directory in zipfile to install", identifier) ); } // Fill in our stanza! stanza.file = candidates[0]; return(stanza); }
public string DescribeInstallStanzas() { List <string> descriptions = new List <string>(); if (install != null) { foreach (ModuleInstallDescriptor mid in install) { descriptions.Add(mid.DescribeMatch()); } } else { descriptions.Add(ModuleInstallDescriptor.DefaultInstallStanza(identifier).DescribeMatch()); } return(string.Join(", ", descriptions)); }
/// <summary> /// Given a stanza and an open zipfile, returns all files that would be installed /// for this stanza. /// /// If a KSP instance is provided, it will be used to generate output paths, otherwise these will be null. /// /// Throws a BadInstallLocationKraken if the install stanza targets an /// unknown install location (eg: not GameData, Ships, etc) /// /// Throws a BadMetadataKraken if the stanza resulted in no files being returned. /// </summary> internal static List <InstallableFile> FindInstallableFiles(ModuleInstallDescriptor stanza, ZipFile zipfile, KSP ksp) { string installDir; bool makeDirs; var files = new List <InstallableFile> (); if (stanza.install_to == "GameData") { installDir = ksp == null ? null : ksp.GameData(); makeDirs = true; } else if (stanza.install_to == "Ships") { installDir = ksp == null ? null : ksp.Ships(); makeDirs = false; // Don't allow directory creation in ships directory } else if (stanza.install_to == "Tutorial") { installDir = ksp == null ? null : ksp.Tutorial(); makeDirs = true; } else if (stanza.install_to == "GameRoot") { installDir = ksp == null ? null : ksp.GameDir(); makeDirs = false; } else { throw new BadInstallLocationKraken("Unknown install_to " + stanza.install_to); } // O(N^2) solution, as we're walking the zipfile for each stanza. // Surely there's a better way, although this is fast enough we may not care. foreach (ZipEntry entry in zipfile) { // Skips things not prescribed by our install stanza. if (!stanza.IsWanted(entry.Name)) { continue; } // Prepare our file info. InstallableFile file_info = new InstallableFile(); file_info.source = entry; file_info.makedir = makeDirs; file_info.destination = null; // If we have a place to install it, fill that in... if (installDir != null) { // Get the full name of the file. string outputName = entry.Name; // Update our file info with the install location file_info.destination = TransformOutputName(stanza.file, outputName, installDir); } files.Add(file_info); } // If we have no files, then something is wrong! (KSP-CKAN/CKAN#93) if (files.Count == 0) { // We have null as the first argument here, because we don't know which module we're installing throw new BadMetadataKraken(null, String.Format("No files found in {0} to install!", stanza.file)); } return(files); }
/// <summary> /// Given a stanza and an open zipfile, returns all files that would be installed /// for this stanza. /// /// If a KSP instance is provided, it will be used to generate output paths, otherwise these will be null. /// /// Throws a BadInstallLocationKraken if the install stanza targets an /// unknown install location (eg: not GameData, Ships, etc) /// /// Throws a BadMetadataKraken if the stanza resulted in no files being returned. /// </summary> /// <exception cref="BadInstallLocationKraken">Thrown when the installation path is not valid according to the spec.</exception> internal static List <InstallableFile> FindInstallableFiles(ModuleInstallDescriptor stanza, ZipFile zipfile, KSP ksp) { string installDir; bool makeDirs; var files = new List <InstallableFile> (); // Normalize the path before doing everything else // TODO: This really should happen in the ModuleInstallDescriptor itself. stanza.install_to = KSPPathUtils.NormalizePath(stanza.install_to); // Convert our stanza to a standard `file` type. This is a no-op if it's // already the basic type. stanza = stanza.ConvertFindToFile(zipfile); if (stanza.install_to == "GameData" || stanza.install_to.StartsWith("GameData/")) { // The installation path can be either "GameData" or a sub-directory of "GameData" // but it cannot contain updirs if (stanza.install_to.Contains("/../") || stanza.install_to.EndsWith("/..")) { throw new BadInstallLocationKraken("Invalid installation path: " + stanza.install_to); } string subDir = stanza.install_to.Substring("GameData".Length); // remove "GameData" subDir = subDir.StartsWith("/") ? subDir.Substring(1) : subDir; // remove a "/" at the beginning, if present // Add the extracted subdirectory to the path of KSP's GameData installDir = ksp == null ? null : (KSPPathUtils.NormalizePath(ksp.GameData() + "/" + subDir)); makeDirs = true; } else if (stanza.install_to.StartsWith("Ships")) { // Don't allow directory creation in ships directory makeDirs = false; switch (stanza.install_to) { case "Ships": installDir = ksp == null ? null : ksp.Ships(); break; case "Ships/VAB": installDir = ksp == null ? null : ksp.ShipsVab(); break; case "Ships/SPH": installDir = ksp == null ? null : ksp.ShipsSph(); break; default: throw new BadInstallLocationKraken("Unknown install_to " + stanza.install_to); } } else { switch (stanza.install_to) { case "Tutorial": installDir = ksp == null ? null : ksp.Tutorial(); makeDirs = true; break; case "Scenarios": installDir = ksp == null ? null : ksp.Scenarios(); makeDirs = true; break; case "GameRoot": installDir = ksp == null ? null : ksp.GameDir(); makeDirs = false; break; default: throw new BadInstallLocationKraken("Unknown install_to " + stanza.install_to); } } // O(N^2) solution, as we're walking the zipfile for each stanza. // Surely there's a better way, although this is fast enough we may not care. foreach (ZipEntry entry in zipfile) { // Skips things not prescribed by our install stanza. if (!stanza.IsWanted(entry.Name)) { continue; } // Prepare our file info. InstallableFile file_info = new InstallableFile { source = entry, makedir = makeDirs, destination = null }; // If we have a place to install it, fill that in... if (installDir != null) { // Get the full name of the file. string outputName = entry.Name; // Update our file info with the install location file_info.destination = TransformOutputName(stanza.file, outputName, installDir); } files.Add(file_info); } // If we have no files, then something is wrong! (KSP-CKAN/CKAN#93) if (files.Count == 0) { // We have null as the first argument here, because we don't know which module we're installing throw new BadMetadataKraken(null, String.Format("No files found in {0} to install!", stanza.file)); } return(files); }
/// <summary> /// Given a stanza and an open zipfile, returns all files that would be installed /// for this stanza. /// /// If a KSP instance is provided, it will be used to generate output paths, otherwise these will be null. /// /// Throws a BadInstallLocationKraken if the install stanza targets an /// unknown install location (eg: not GameData, Ships, etc) /// /// Throws a BadMetadataKraken if the stanza resulted in no files being returned. /// </summary> /// <exception cref="BadInstallLocationKraken">Thrown when the installation path is not valid according to the spec.</exception> internal static List<InstallableFile> FindInstallableFiles(ModuleInstallDescriptor stanza, ZipFile zipfile, KSP ksp) { string installDir; bool makeDirs; var files = new List<InstallableFile> (); // Normalize the path before doing everything else // TODO: This really should happen in the ModuleInstallDescriptor itself. stanza.install_to = KSPPathUtils.NormalizePath(stanza.install_to); // Convert our stanza to a standard `file` type. This is a no-op if it's // already the basic type. stanza = stanza.ConvertFindToFile(zipfile); if (stanza.install_to == "GameData" || stanza.install_to.StartsWith("GameData/")) { // The installation path can be either "GameData" or a sub-directory of "GameData" // but it cannot contain updirs if (stanza.install_to.Contains("/../") || stanza.install_to.EndsWith("/..")) throw new BadInstallLocationKraken("Invalid installation path: " + stanza.install_to); string subDir = stanza.install_to.Substring("GameData".Length); // remove "GameData" subDir = subDir.StartsWith("/") ? subDir.Substring(1) : subDir; // remove a "/" at the beginning, if present // Add the extracted subdirectory to the path of KSP's GameData installDir = ksp == null ? null : (KSPPathUtils.NormalizePath(ksp.GameData() + "/" + subDir)); makeDirs = true; } else if (stanza.install_to.StartsWith("Ships")) { // Don't allow directory creation in ships directory makeDirs = false; switch (stanza.install_to) { case "Ships": installDir = ksp == null ? null : ksp.Ships(); break; case "Ships/VAB": installDir = ksp == null ? null : ksp.ShipsVab(); break; case "Ships/SPH": installDir = ksp == null ? null : ksp.ShipsSph(); break; case "Ships/@thumbs": installDir = ksp == null ? null : ksp.ShipsThumbs(); break; case "Ships/@thumbs/VAB": installDir = ksp == null ? null : ksp.ShipsThumbsVAB(); break; case "Ships/@thumbs/SPH": installDir = ksp == null ? null : ksp.ShipsThumbsSPH(); break; default: throw new BadInstallLocationKraken("Unknown install_to " + stanza.install_to); } } else switch (stanza.install_to) { case "Tutorial": installDir = ksp == null ? null : ksp.Tutorial(); makeDirs = true; break; case "Scenarios": installDir = ksp == null ? null : ksp.Scenarios(); makeDirs = true; break; case "GameRoot": installDir = ksp == null ? null : ksp.GameDir(); makeDirs = false; break; default: throw new BadInstallLocationKraken("Unknown install_to " + stanza.install_to); } // O(N^2) solution, as we're walking the zipfile for each stanza. // Surely there's a better way, although this is fast enough we may not care. foreach (ZipEntry entry in zipfile) { // Skips things not prescribed by our install stanza. if (! stanza.IsWanted(entry.Name)) { continue; } // Prepare our file info. InstallableFile file_info = new InstallableFile { source = entry, makedir = makeDirs, destination = null }; // If we have a place to install it, fill that in... if (installDir != null) { // Get the full name of the file. string outputName = entry.Name; // Update our file info with the install location file_info.destination = TransformOutputName(stanza.file, outputName, installDir); } files.Add(file_info); } // If we have no files, then something is wrong! (KSP-CKAN/CKAN#93) if (files.Count == 0) { // We have null as the first argument here, because we don't know which module we're installing throw new BadMetadataKraken(null, String.Format("No files found in {0} to install!", stanza.file)); } return files; }
private static void TestDogeCoinStanza(ModuleInstallDescriptor stanza) { Assert.AreEqual("GameData", stanza.install_to); Assert.AreEqual("DogeCoinFlag-1.01/GameData/DogeCoinFlag", stanza.file); }
/// <summary> /// Compare two install stanzas /// </summary> /// <param name="other">The other stanza for comparison</param> /// <returns> /// True if they're equivalent, false if they're different /// </returns> public override bool Equals(object other) { ModuleInstallDescriptor otherStanza = other as ModuleInstallDescriptor; if (otherStanza == null) { // Not even the right type! return(false); } if (KSPPathUtils.NormalizePath(file) != KSPPathUtils.NormalizePath(otherStanza.file)) { return(false); } if (KSPPathUtils.NormalizePath(find) != KSPPathUtils.NormalizePath(otherStanza.find)) { return(false); } if (find_regexp != otherStanza.find_regexp) { return(false); } if (KSPPathUtils.NormalizePath(install_to) != KSPPathUtils.NormalizePath(otherStanza.install_to)) { return(false); } if (@as != otherStanza.@as) { return(false); } if ((filter == null) != (otherStanza.filter == null)) { return(false); } if (filter != null && !filter.SequenceEqual(otherStanza.filter)) { return(false); } if ((filter_regexp == null) != (otherStanza.filter_regexp == null)) { return(false); } if (filter_regexp != null && !filter_regexp.SequenceEqual(otherStanza.filter_regexp)) { return(false); } if (find_matches_files != otherStanza.find_matches_files) { return(false); } if ((include_only == null) != (otherStanza.include_only == null)) { return(false); } if (include_only != null && !include_only.SequenceEqual(otherStanza.include_only)) { return(false); } if ((include_only_regexp == null) != (otherStanza.include_only_regexp == null)) { return(false); } if (include_only_regexp != null && !include_only_regexp.SequenceEqual(otherStanza.include_only_regexp)) { return(false); } return(true); }
private void TestDogeCoinStanza(CKAN.ModuleInstallDescriptor stanza) { Assert.AreEqual("GameData", stanza.install_to); Assert.AreEqual("DogeCoinFlag-1.01/GameData/DogeCoinFlag", stanza.file); }