// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Globalization; using System.Text; using System.Text.RegularExpressions; using EpicGames.Core; namespace HordeServer.Utilities { /// /// Stores a mapping from one set of paths to another /// public class ViewMap { /// /// List of entries making up the view /// public List Entries { get; } /// /// Default constructor /// public ViewMap() { Entries = new List(); } /// /// Constructor /// /// public ViewMap(ViewMap other) { Entries = new List(other.Entries); } /// /// Determines if a file is included in the view /// /// The file to test /// The comparison type /// True if the file is included in the view public bool MatchFile(Utf8String file, Utf8StringComparer comparison) { bool included = false; foreach (ViewMapEntry entry in Entries) { if (entry.MatchFile(file, comparison)) { included = entry.Include; } } return included; } /// /// Attempts to convert a source file to its target path /// /// /// The comparison type /// /// public bool TryMapFile(Utf8String sourceFile, Utf8StringComparer comparison, out Utf8String targetFile) { ViewMapEntry? mapEntry = null; foreach (ViewMapEntry entry in Entries) { if (entry.MatchFile(sourceFile, comparison)) { mapEntry = entry; } } if (mapEntry != null && mapEntry.Include) { targetFile = mapEntry.MapFile(sourceFile); return true; } else { targetFile = Utf8String.Empty; return false; } } /// /// Gets the root paths from the view entries /// /// public List GetRootPaths(Utf8StringComparer comparison) { List rootPaths = new List(); foreach (ViewMapEntry entry in Entries) { if (entry.Include) { int lastSlashIdx = entry.SourcePrefix.LastIndexOf('/'); Utf8String rootPath = entry.SourcePrefix.Slice(0, lastSlashIdx + 1); for (int idx = 0; ; idx++) { if (idx == rootPaths.Count) { rootPaths.Add(rootPath); break; } else if (rootPaths[idx].StartsWith(rootPath, comparison)) { rootPaths[idx] = rootPath; break; } else if (rootPath.StartsWith(rootPaths[idx], comparison)) { break; } } } } return rootPaths; } } /// /// Entry within a ViewMap /// public class ViewMapEntry { /// /// Whether to include files matching this pattern /// public bool Include { get; } /// /// The wildcard string - either '*' or '...' /// public Utf8String Wildcard { get; } /// /// The source part of the pattern before the wildcard /// public Utf8String SourcePrefix { get; } /// /// The source part of the pattern after the wildcard. Perforce does not permit a slash to be in this part of the mapping. /// public Utf8String SourceSuffix { get; } /// /// The target mapping for the pattern before the wildcard /// public Utf8String TargetPrefix { get; } /// /// The target mapping for the pattern after the wildcard /// public Utf8String TargetSuffix { get; } /// /// The full source pattern /// public Utf8String Source => new Utf8String($"{SourcePrefix}{Wildcard}{SourceSuffix}"); /// /// The full target pattern /// public Utf8String Target => new Utf8String($"{TargetPrefix}{Wildcard}{TargetSuffix}"); /// /// Tests if the entry has a file wildcard ('*') /// /// True if the entry has a file wildcard public bool IsFileWildcard() => Wildcard.Length == 1; /// /// Tests if the entry has a path wildcard ('...') /// /// True if the entry has a path wildcard public bool IsPathWildcard() => Wildcard.Length == 3; /// /// Constructor /// /// public ViewMapEntry(ViewMapEntry other) : this(other.Include, other.Wildcard, other.SourcePrefix, other.SourceSuffix, other.TargetPrefix, other.TargetSuffix) { } /// /// Constructor /// /// /// /// public ViewMapEntry(bool include, string source, string target) { Include = include; Match match = Regex.Match(source, @"^(.*)(\*|\.\.\.|%%1)(.*)$"); if (match.Success) { string wildcardStr = match.Groups[2].Value; SourcePrefix = new Utf8String(match.Groups[1].Value); SourceSuffix = new Utf8String(match.Groups[3].Value); Wildcard = new Utf8String(match.Groups[2].Value); int otherIdx = target.IndexOf(wildcardStr, StringComparison.Ordinal); TargetPrefix = new Utf8String(target.Substring(0, otherIdx)); TargetSuffix = new Utf8String(target.Substring(otherIdx + Wildcard.Length)); if (wildcardStr.Equals("%%1", StringComparison.Ordinal)) { Wildcard = new Utf8String("*"); } } else { SourcePrefix = new Utf8String(source); SourceSuffix = Utf8String.Empty; TargetPrefix = new Utf8String(target); TargetSuffix = Utf8String.Empty; } } /// /// Constructor /// /// /// /// /// /// /// public ViewMapEntry(bool include, Utf8String wildcard, Utf8String sourcePrefix, Utf8String sourceSuffix, Utf8String targetPrefix, Utf8String targetSuffix) { Include = include; Wildcard = wildcard; SourcePrefix = sourcePrefix; SourceSuffix = sourceSuffix; TargetPrefix = targetPrefix; TargetSuffix = targetSuffix; } /// /// Maps a file to the target path /// /// /// public Utf8String MapFile(Utf8String sourceFile) { int count = sourceFile.Length - SourceSuffix.Length - SourcePrefix.Length; return TargetPrefix + sourceFile.Slice(SourcePrefix.Length, count) + TargetSuffix; } /// /// Determine if a file matches the current entry /// /// Path to the file /// The comparison type /// True if the path matches the entry public bool MatchFile(Utf8String path, Utf8StringComparer comparison) { if (Wildcard.Length == 0) { return comparison.Compare(path, SourcePrefix) == 0; } else { if (!path.StartsWith(SourcePrefix, comparison) || !path.EndsWith(SourceSuffix, comparison)) { return false; } if (IsFileWildcard() && path.Slice(SourcePrefix.Length, path.Length - SourceSuffix.Length - SourcePrefix.Length).IndexOf('/') != -1) { return false; } return true; } } /// public override string ToString() { StringBuilder builder = new StringBuilder(); if (!Include) { builder.Append('-'); } builder.Append(CultureInfo.InvariantCulture, $"{SourcePrefix}{Wildcard}{SourceSuffix} {TargetPrefix}{Wildcard}{TargetSuffix}"); return builder.ToString(); } } }