using System; using System.Collections.Generic; using System.Text; using System.Text.RegularExpressions; namespace Deploymentcenter.Client { /// /// Abgleich von Pfaden gegen Glob-Muster. /// /// Der Packager verstand zuvor nur "*.endung" und exakte Namen. Muster wie /// "logs/**" oder "wwwroot/cache/*" trafen deshalb nie zu - sie standen in /// der mitgelieferten Beispielkonfiguration und weckten den Eindruck, die /// betreffenden Dateien seien ausgeschlossen. Ein Konfigurationsfile, das /// so unbemerkt ins Paket rutscht, ueberschreibt beim naechsten Update die /// Einstellungen jedes Zielsystems. /// /// Regeln, angelehnt an .gitignore: /// * trifft beliebig viele Zeichen ausser dem Trenner / /// ** trifft beliebig viele Zeichen einschliesslich / /// ? trifft genau ein Zeichen ausser / /// Muster ohne / werden gegen den Dateinamen geprueft, /// Muster mit / gegen den vollstaendigen relativen Pfad. /// Ein Muster, das auf / endet, trifft alles unterhalb dieses Ordners. /// /// Verglichen wird ohne Beachtung der Gross-/Kleinschreibung: Ziel sind /// Windows- und Linux-Systeme gleichermassen, und ein Muster, das nur auf /// einer der beiden Plattformen greift, ist gefaehrlicher als ein Muster, /// das etwas zu viel trifft. /// public static class GlobMatcher { private static readonly Dictionary Cache = new Dictionary(StringComparer.Ordinal); private static readonly object CacheLock = new object(); /// /// Prueft einen relativen Pfad gegen eine Liste von Mustern. /// Leere Listen treffen nie. /// public static bool IsMatch(string relativePath, IEnumerable? patterns) { if (patterns == null || string.IsNullOrEmpty(relativePath)) return false; string normalized = Normalize(relativePath); foreach (var pattern in patterns) { if (IsMatch(normalized, pattern, alreadyNormalized: true)) return true; } return false; } /// Prueft einen relativen Pfad gegen ein einzelnes Muster. public static bool IsMatch(string relativePath, string? pattern, bool alreadyNormalized = false) { if (string.IsNullOrWhiteSpace(pattern) || string.IsNullOrEmpty(relativePath)) return false; string path = alreadyNormalized ? relativePath : Normalize(relativePath); string trimmed = pattern!.Trim(); if (trimmed.Length == 0) return false; // Ein fuehrendes ./ oder / bedeutet "ab Wurzel" und ist fuer den // Vergleich mit einem ohnehin relativen Pfad ohne Bedeutung. if (trimmed.StartsWith("./", StringComparison.Ordinal)) trimmed = trimmed.Substring(2); else if (trimmed.StartsWith("/", StringComparison.Ordinal)) trimmed = trimmed.Substring(1); // "logs/" meint alles unterhalb von logs. if (trimmed.EndsWith("/", StringComparison.Ordinal)) trimmed += "**"; var regex = GetRegex(trimmed); // Muster ohne Trenner gelten fuer den Dateinamen an beliebiger // Stelle im Baum - "*.pdb" soll auch runtimes/x/y.pdb treffen. if (trimmed.IndexOf('/') < 0) { int slash = path.LastIndexOf('/'); string fileName = slash >= 0 ? path.Substring(slash + 1) : path; return regex.IsMatch(fileName); } return regex.IsMatch(path); } /// Vereinheitlicht Trenner und entfernt ein fuehrendes ./ public static string Normalize(string path) { string value = (path ?? string.Empty).Replace('\\', '/').TrimStart(); if (value.StartsWith("./", StringComparison.Ordinal)) value = value.Substring(2); return value; } private static Regex GetRegex(string pattern) { lock (CacheLock) { if (Cache.TryGetValue(pattern, out var cached)) return cached; var regex = new Regex( "^" + Translate(pattern) + "$", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); // Die Musterlisten stammen aus Konfigurationsdateien und sind // klein; die Obergrenze verhindert nur unbegrenztes Wachsen, // falls doch einmal dynamisch erzeugte Muster hereinkommen. if (Cache.Count < 512) Cache[pattern] = regex; return regex; } } /// Uebersetzt ein Glob-Muster in einen regulaeren Ausdruck. private static string Translate(string pattern) { var builder = new StringBuilder(); for (int i = 0; i < pattern.Length; i++) { char c = pattern[i]; switch (c) { case '*': bool doubleStar = i + 1 < pattern.Length && pattern[i + 1] == '*'; if (doubleStar) { i++; // "a/**/b" muss auch "a/b" treffen, sonst waere ein // Muster wie "logs/**" auf den Ordner selbst blind. if (i + 1 < pattern.Length && pattern[i + 1] == '/') { i++; builder.Append("(?:.*/)?"); } else { builder.Append(".*"); } } else { builder.Append("[^/]*"); } break; case '?': builder.Append("[^/]"); break; default: builder.Append(Regex.Escape(c.ToString())); break; } } return builder.ToString(); } } }