// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Text.RegularExpressions; namespace EpicGames.Perforce { /// /// Stores a mapping from one set of paths to another /// public class PerforceViewMap { /// /// List of entries making up the view /// public List Entries { get; } /// /// Default constructor /// public PerforceViewMap() { Entries = new List(); } /// /// Construct from an existing set of entries /// /// public PerforceViewMap(IEnumerable entries) { Entries = entries.ToList(); } /// /// Constructor /// /// public PerforceViewMap(PerforceViewMap other) { Entries = new List(other.Entries); } /// /// Construct a view map from a set of entries /// /// public static PerforceViewMap Parse(IEnumerable entries) { return new PerforceViewMap(entries.Select(x => PerforceViewMapEntry.Parse(x))); } /// /// Gets the inverse of this mapping /// /// public PerforceViewMap Invert() { List entries = new List(); foreach (PerforceViewMapEntry entry in Entries) { entries.Add(new PerforceViewMapEntry(entry.Include, entry.Target, entry.Source)); } return new PerforceViewMap(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(string file, StringComparison comparison) { bool included = false; foreach (PerforceViewMapEntry entry in Entries) { if (entry.MatchFile(file, comparison)) { included = entry.Include; } } return included; } /// /// Maps a set of files into the target files /// /// List of source files /// Comparison to use for strings /// List of files in the target domain, excluding any not covered by the mapping public IEnumerable MapFiles(IEnumerable sourceFiles, StringComparison comparison) { foreach (string sourceFile in sourceFiles) { if (TryMapFile(sourceFile, comparison, out string? targetFile)) { yield return targetFile; } } } /// /// Attempts to convert a source file to its target path /// /// /// The comparison type /// /// public bool TryMapFile(string sourceFile, StringComparison comparison, out string targetFile) { PerforceViewMapEntry? mapEntry = null; foreach (PerforceViewMapEntry entry in Entries) { if (entry.MatchFile(sourceFile, comparison)) { mapEntry = entry; } } if (mapEntry != null && mapEntry.Include) { targetFile = mapEntry.MapFile(sourceFile); return true; } else { targetFile = String.Empty; return false; } } /// /// Gets the root paths from the view entries /// /// public List GetRootPaths(StringComparison comparison) { List rootPaths = new List(); foreach (PerforceViewMapEntry entry in Entries) { if (entry.Include) { int lastSlashIdx = entry.SourcePrefix.LastIndexOf('/'); ReadOnlySpan rootPath = entry.SourcePrefix.AsSpan(0, lastSlashIdx + 1); for (int idx = 0; ; idx++) { if (idx == rootPaths.Count) { rootPaths.Add(rootPath.ToString()); break; } else if (rootPaths[idx].AsSpan().StartsWith(rootPath, comparison)) { rootPaths[idx] = rootPath.ToString(); break; } else if (rootPath.StartsWith(rootPaths[idx], comparison)) { break; } } } } return rootPaths; } } /// /// Entry within a ViewMap /// public class PerforceViewMapEntry { /// /// Whether to include files matching this pattern /// public bool Include { get; } /// /// The wildcard string - either '*' or '...' /// public string Wildcard { get; } /// /// The source part of the pattern before the wildcard /// public string 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 string SourceSuffix { get; } /// /// The target mapping for the pattern before the wildcard /// public string TargetPrefix { get; } /// /// The target mapping for the pattern after the wildcard /// public string TargetSuffix { get; } /// /// The full source pattern /// public string Source => $"{SourcePrefix}{Wildcard}{SourceSuffix}"; /// /// The full target pattern /// public string Target => $"{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 PerforceViewMapEntry(PerforceViewMapEntry other) : this(other.Include, other.Wildcard, other.SourcePrefix, other.SourceSuffix, other.TargetPrefix, other.TargetSuffix) { } /// /// Constructor /// /// /// /// public PerforceViewMapEntry(bool include, string source, string target) { Include = include; Match match = Regex.Match(source, @"^(.*?)(\*|\.\.\.|%%1)(.*)$"); if (match.Success) { string wildcardStr = match.Groups[2].Value; SourcePrefix = match.Groups[1].Value; SourceSuffix = match.Groups[3].Value; Wildcard = match.Groups[2].Value; int otherIdx = target.IndexOf(wildcardStr, StringComparison.Ordinal); TargetPrefix = target.Substring(0, otherIdx); TargetSuffix = target.Substring(otherIdx + Wildcard.Length); if (wildcardStr.Equals("%%1", StringComparison.Ordinal)) { Wildcard = "*"; } } else { SourcePrefix = source; SourceSuffix = String.Empty; TargetPrefix = target; TargetSuffix = String.Empty; Wildcard = String.Empty; } } /// /// Constructor /// /// /// /// /// /// /// public PerforceViewMapEntry(bool include, string wildcard, string sourcePrefix, string sourceSuffix, string targetPrefix, string targetSuffix) { Include = include; Wildcard = wildcard; SourcePrefix = sourcePrefix; SourceSuffix = sourceSuffix; TargetPrefix = targetPrefix; TargetSuffix = targetSuffix; } /// /// Parse a view map entry from a string, as returned by spec documents /// /// /// public static PerforceViewMapEntry Parse(string entry) { Match match = Regex.Match(entry, @"^\s*(-?)\s*([^ ]+)\s+([^ ]+)\s*$"); if (!match.Success) { throw new PerforceException($"Unable to parse view map entry: {entry}"); } return new PerforceViewMapEntry(match.Groups[1].Length == 0, match.Groups[2].Value, match.Groups[3].Value); } /// /// Maps a file to the target path /// /// /// public string MapFile(string sourceFile) { int count = sourceFile.Length - SourceSuffix.Length - SourcePrefix.Length; return String.Concat(TargetPrefix, sourceFile.AsSpan(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(string path, StringComparison comparison) { if (Wildcard.Length == 0) { return String.Equals(path, SourcePrefix, comparison); } else { if (!path.StartsWith(SourcePrefix, comparison) || !path.EndsWith(SourceSuffix, comparison)) { return false; } if (IsFileWildcard() && path.AsSpan(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($"{SourcePrefix}{Wildcard}{SourceSuffix} {TargetPrefix}{Wildcard}{TargetSuffix}"); return builder.ToString(); } } }