private void EnsurePattern() { if (inst_pattern == null) { if (file != null) { file = CKANPathUtils.NormalizePath(file); inst_pattern = new Regex(@"^" + Regex.Escape(file) + @"(/|$)", RegexOptions.IgnoreCase | RegexOptions.Compiled); } else if (find != null) { find = CKANPathUtils.NormalizePath(find); inst_pattern = new Regex(@"(?:^|/)" + Regex.Escape(find) + @"(/|$)", RegexOptions.IgnoreCase | RegexOptions.Compiled); } else if (find_regexp != null) { inst_pattern = new Regex(find_regexp, RegexOptions.IgnoreCase | RegexOptions.Compiled); } else { throw new UnsupportedKraken("Install stanzas requires `file` or `find` or `find_regexp`."); } } }
internal void DeSerialisationFixes(StreamingContext like_i_could_care) { // Make sure our install_to fields exists. We may be able to remove // this check now that we're doing better json-fu above. if (install_to == null) { throw new BadMetadataKraken(null, "Install stanzas must have an install_to"); } var setCount = new[] { file, find, find_regexp }.Count(i => i != null); // Make sure we have either a `file`, `find`, or `find_regexp` stanza. if (setCount == 0) { throw new BadMetadataKraken(null, "Install stanzas require either a file, find, or find_regexp directive"); } if (setCount > 1) { throw new BadMetadataKraken(null, "Install stanzas must only include one of file, find, or find_regexp directives"); } // Make sure only filter or include_only fields exist but not both at the same time var filterCount = new[] { filter, filter_regexp }.Count(i => i != null); var includeOnlyCount = new[] { include_only, include_only_regexp }.Count(i => i != null); if (filterCount > 0 && includeOnlyCount > 0) { throw new BadMetadataKraken(null, "Install stanzas can only contain filter or include_only directives, not both"); } // Normalize paths on load (note, doesn't cover assignment like in tests) install_to = CKANPathUtils.NormalizePath(install_to); }
public string CkanDir() { if (!Valid) { log.Error("Could not find KSP version"); throw new NotKSPDirKraken(gameDir, "Could not find KSP version in buildID.txt or readme.txt"); } return(CKANPathUtils.NormalizePath( Path.Combine(GameDir(), "CKAN"))); }
/// <summary> /// Transforms the name of the output. This will strip the leading directories from the stanza file from /// output name and then combine it with the installDir. /// EX: "kOS-1.1/GameData/kOS", "kOS-1.1/GameData/kOS/Plugins/kOS.dll", "GameData" will be transformed /// to "GameData/kOS/Plugins/kOS.dll" /// </summary> /// <param name="outputName">The name of the file to transform</param> /// <param name="installDir">The installation dir where the file should end up with</param> /// <returns>The output name</returns> internal string TransformOutputName(IGame game, string outputName, string installDir, string @as) { string leadingPathToRemove = Path .GetDirectoryName(ShortestMatchingPrefix(outputName)) .Replace('\\', '/'); if (!string.IsNullOrEmpty(leadingPathToRemove)) { Regex leadingRE = new Regex( "^" + Regex.Escape(leadingPathToRemove) + "/", RegexOptions.Compiled); if (!leadingRE.IsMatch(outputName)) { throw new BadMetadataKraken(null, String.Format( "Output file name ({0}) not matching leading path of stanza ({1})", outputName, leadingPathToRemove)); } // Strip off leading path name outputName = leadingRE.Replace(outputName, ""); } // Now outputname looks like PATH/what/ever/file.ext, where // PATH is the part that matched `file` or `find` or `find_regexp` if (!string.IsNullOrWhiteSpace(@as)) { if (@as.Contains("/") || @as.Contains("\\")) { throw new BadMetadataKraken(null, "`as` may not include path separators."); } // Replace first path component with @as outputName = ReplaceFirstPiece(outputName, "/", @as); } else { var reservedPrefix = game.ReservedPaths.FirstOrDefault(prefix => outputName.StartsWith(prefix + "/", StringComparison.InvariantCultureIgnoreCase)); if (reservedPrefix != null) { // If we try to install a folder with the same name as // one of the reserved directories, strip it off. // Delete reservedPrefix and one forward slash outputName = outputName.Substring(reservedPrefix.Length + 1); } } // Return our snipped, normalised, and ready to go output filename! return(CKANPathUtils.NormalizePath( Path.Combine(installDir, outputName) )); }
/// <summary> /// Ensures all files for this module have relative paths. /// Called when upgrading registry versions. Should be a no-op /// if called on newer registries. /// </summary> public void Renormalise(GameInstance ksp) { var normalised_installed_files = new Dictionary <string, InstalledModuleFile>(); foreach (KeyValuePair <string, InstalledModuleFile> tuple in installed_files) { string path = CKANPathUtils.NormalizePath(tuple.Key); if (Path.IsPathRooted(path)) { path = ksp.ToRelativeGameDir(path); } normalised_installed_files[path] = tuple.Value; } installed_files = normalised_installed_files; }
/// <summary> /// Ensures all files for this module have relative paths. /// Called when upgrading registry versions. Should be a no-op /// if called on newer registries. /// </summary> public void Renormalise(GameInstance ksp) { // We need case insensitive path matching on Windows var normalised_installed_files = Platform.IsWindows ? new Dictionary <string, InstalledModuleFile>(StringComparer.OrdinalIgnoreCase) : new Dictionary <string, InstalledModuleFile>(); foreach (KeyValuePair <string, InstalledModuleFile> tuple in installed_files) { string path = CKANPathUtils.NormalizePath(tuple.Key); if (Path.IsPathRooted(path)) { path = ksp.ToRelativeGameDir(path); } normalised_installed_files[path] = tuple.Value; } installed_files = normalised_installed_files; }
/// <summary> /// Returns a KSP object. /// Will initialise a CKAN instance in the KSP dir if it does not already exist, /// if the directory contains a valid KSP install. /// </summary> public GameInstance(IGame game, string gameDir, string name, IUser user, bool scan = true) { this.game = game; Name = name; User = user; // Make sure our path is absolute and has normalised slashes. this.gameDir = CKANPathUtils.NormalizePath(Path.GetFullPath(gameDir)); if (Platform.IsWindows) { // Normalized slashes are bad for pure drive letters, // Path.Combine turns them into drive-relative paths like // K:GameData/whatever if (Regex.IsMatch(this.gameDir, @"^[a-zA-Z]:$")) { this.gameDir = $"{this.gameDir}/"; } } if (Valid) { SetupCkanDirectories(scan); LoadCompatibleVersions(); } }
/// <summary> /// Given a path relative to this KSP's GameDir, returns the /// absolute path on the system. /// </summary> public string ToAbsoluteGameDir(string path) { return(CKANPathUtils.ToAbsolute(path, GameDir())); }
/// <summary> /// Returns path relative to this KSP's GameDir. /// </summary> public string ToRelativeGameDir(string path) { return(CKANPathUtils.ToRelative(path, GameDir())); }
public string TempDir() { return(CKANPathUtils.NormalizePath( Path.Combine(CkanDir(), "temp") )); }
public string InstallHistoryDir() { return(CKANPathUtils.NormalizePath( Path.Combine(CkanDir(), "history") )); }
public string DownloadCacheDir() { return(CKANPathUtils.NormalizePath( Path.Combine(CkanDir(), "downloads"))); }
/// <summary> /// Given 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> public List <InstallableFile> FindInstallableFiles(ZipFile zipfile, GameInstance ksp) { string installDir; var files = new List <InstallableFile>(); // Normalize the path before doing everything else string install_to = CKANPathUtils.NormalizePath(this.install_to); // The installation path cannot contain updirs if (install_to.Contains("/../") || install_to.EndsWith("/..")) { throw new BadInstallLocationKraken("Invalid installation path: " + install_to); } if (ksp == null) { installDir = null; } else if (install_to == ksp.game.PrimaryModDirectoryRelative || install_to.StartsWith($"{ksp.game.PrimaryModDirectoryRelative}/")) { // The installation path can be either "GameData" or a sub-directory of "GameData" string subDir = install_to.Substring(ksp.game.PrimaryModDirectoryRelative.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 = CKANPathUtils.NormalizePath(ksp.game.PrimaryModDirectory(ksp) + "/" + subDir); } else { switch (install_to) { case "GameRoot": installDir = ksp.GameDir(); break; default: if (ksp.game.AllowInstallationIn(install_to, out string path)) { installDir = ksp.ToAbsoluteGameDir(path); } else { throw new BadInstallLocationKraken("Unknown install_to " + install_to); } break; } } EnsurePattern(); // `find` is supposed to match the "topmost" folder. Find it. var shortestMatch = find == null ? (int?)null : zipfile.Cast <ZipEntry>() .Select(entry => inst_pattern.Match(entry.Name.Replace('\\', '/'))) .Where(match => match.Success) .DefaultIfEmpty() .Min(match => match?.Index); // 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 dirs and things not prescribed by our install stanza. if (!IsWanted(entry.Name, shortestMatch)) { continue; } // Prepare our file info. InstallableFile file_info = new InstallableFile { source = entry, makedir = false, destination = null }; // If we have a place to install it, fill that in... if (installDir != null) { // Get the full name of the file. // Update our file info with the install location file_info.destination = TransformOutputName( ksp.game, entry.Name, installDir, @as); file_info.makedir = AllowDirectoryCreation( ksp.game, ksp?.ToRelativeGameDir(file_info.destination) ?? file_info.destination); } 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 matching {0} to install!", DescribeMatch())); } return(files); }
/// <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. /// IEquatable<> uses this for more efficient comparisons. /// </returns> public bool Equals(ModuleInstallDescriptor otherStanza) { if (otherStanza == null) { // Not even the right type! return(false); } if (CKANPathUtils.NormalizePath(file) != CKANPathUtils.NormalizePath(otherStanza.file)) { return(false); } if (CKANPathUtils.NormalizePath(find) != CKANPathUtils.NormalizePath(otherStanza.find)) { return(false); } if (find_regexp != otherStanza.find_regexp) { return(false); } if (CKANPathUtils.NormalizePath(install_to) != CKANPathUtils.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); }