// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using EpicGames.Core; namespace EpicGames.Perforce { /// /// Utility methods for dealing with Perforce paths /// public static class PerforceUtils { /// /// Set of extensions to treat as code /// public static readonly HashSet CodeExtensions = new HashSet { ".c", ".cc", ".cpp", ".inl", ".m", ".mm", ".rc", ".cs", ".csproj", ".h", ".hpp", ".inl", ".usf", ".ush", ".uproject", ".uplugin", ".sln", ".native.verse" }; /// /// Tests if a path is a code file /// /// Path to test /// True if the path is a code file public static bool IsCodeFile(string path) { foreach (string codeExtension in CodeExtensions) { if (path.EndsWith(codeExtension, StringComparison.OrdinalIgnoreCase)) { return true; } } return false; } /// /// Escape a path to Perforce syntax /// public static string EscapePath(string path) { string newPath = path; newPath = newPath.Replace("%", "%25", StringComparison.Ordinal); newPath = newPath.Replace("*", "%2A", StringComparison.Ordinal); newPath = newPath.Replace("#", "%23", StringComparison.Ordinal); newPath = newPath.Replace("@", "%40", StringComparison.Ordinal); return newPath; } /// /// Remove escape characters from a path /// public static string UnescapePath(string path) { string newPath = path; newPath = newPath.Replace("%40", "@", StringComparison.Ordinal); newPath = newPath.Replace("%23", "#", StringComparison.Ordinal); newPath = newPath.Replace("%2A", "*", StringComparison.Ordinal); newPath = newPath.Replace("%2a", "*", StringComparison.Ordinal); newPath = newPath.Replace("%25", "%", StringComparison.Ordinal); return newPath; } /// /// Remove escape characters from a UTF8 path /// public static Utf8String UnescapePath(Utf8String path) { ReadOnlySpan pathSpan = path.Span; for (int inputIdx = 0; inputIdx < pathSpan.Length - 2; inputIdx++) { if (pathSpan[inputIdx] == '%') { // Allocate the output buffer byte[] buffer = new byte[path.Length]; pathSpan.Slice(0, inputIdx).CopyTo(buffer.AsSpan()); // Copy the data to the output buffer int outputIdx = inputIdx; while (inputIdx < pathSpan.Length) { // Parse the character code int value = StringUtils.ParseHexByte(pathSpan, inputIdx + 1); if (value == -1) { buffer[outputIdx++] = (byte)'%'; inputIdx++; } else { buffer[outputIdx++] = (byte)value; inputIdx += 3; } // Keep copying until we get to another percent character while (inputIdx < pathSpan.Length && (pathSpan[inputIdx] != '%' || inputIdx + 2 >= pathSpan.Length)) { buffer[outputIdx++] = pathSpan[inputIdx++]; } } // Copy the last chunk of data to the output buffer path = new Utf8String(buffer.AsMemory(0, outputIdx)); break; } } return path; } /// /// /// /// /// /// public static bool TryGetDepotName(string depotPath, [NotNullWhen(true)] out string? depotName) { return TryGetClientName(depotPath, out depotName); } /// /// /// /// /// /// public static bool TryGetClientName(string clientPath, [NotNullWhen(true)] out string? clientName) { if (!clientPath.StartsWith("//", StringComparison.Ordinal)) { clientName = null; return false; } int slashIdx = clientPath.IndexOf('/', 2); if (slashIdx == -1) { clientName = null; return false; } clientName = clientPath.Substring(2, slashIdx - 2); return true; } /// /// /// /// /// public static string GetClientOrDepotDirectoryName(string clientFile) { int index = clientFile.LastIndexOf('/'); if (index == -1) { return ""; } else { return clientFile.Substring(0, index); } } /// /// Get the relative path of a client file (eg. //ClientName/Foo/Bar.txt -> Foo/Bar.txt) /// /// Path to the client file /// /// public static string GetClientRelativePath(string clientFile) { if (!clientFile.StartsWith("//", StringComparison.Ordinal)) { throw new ArgumentException("Invalid client path", nameof(clientFile)); } int idx = clientFile.IndexOf('/', 2); if (idx == -1) { throw new ArgumentException("Invalid client path", nameof(clientFile)); } return clientFile.Substring(idx + 1); } /// /// Get the relative path within a client from a filename /// /// Dierctory containing the file /// File to get the path for /// public static string GetClientRelativePath(DirectoryReference workspaceRoot, FileReference workspaceFile) { if (!workspaceFile.IsUnderDirectory(workspaceRoot)) { throw new ArgumentException("File is not under workspace root", nameof(workspaceFile)); } return workspaceFile.MakeRelativeTo(workspaceRoot).Replace(Path.DirectorySeparatorChar, '/'); } } }