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();
}
}
}