Various C# path utility helper methods.
// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.DotNet.Cli.Utils;
using Microsoft.DotNet.PlatformAbstractions;
namespace Microsoft.DotNet.Tools.Common
{
/// <summary>
/// Various path utility helper methods.
/// </summary>
/// <remarks>
/// https://github.com/dotnet/cli/blob/master/src/Microsoft.DotNet.Cli.Utils/PathUtility.cs
/// </remarks>
public static class PathUtility
{
public static bool IsPlaceholderFile(string path)
{
return string.Equals(Path.GetFileName(path), "_._", StringComparison.Ordinal);
}
public static bool IsChildOfDirectory(string dir, string candidate)
{
if (dir == null)
{
throw new ArgumentNullException(nameof(dir));
}
if (candidate == null)
{
throw new ArgumentNullException(nameof(candidate));
}
dir = Path.GetFullPath(dir);
dir = EnsureTrailingSlash(dir);
candidate = Path.GetFullPath(candidate);
return candidate.StartsWith(dir, StringComparison.OrdinalIgnoreCase);
}
public static string EnsureTrailingSlash(string path)
{
return EnsureTrailingCharacter(path, Path.DirectorySeparatorChar);
}
public static string EnsureTrailingForwardSlash(string path)
{
return EnsureTrailingCharacter(path, '/');
}
private static string EnsureTrailingCharacter(string path, char trailingCharacter)
{
if (path == null)
{
throw new ArgumentNullException(nameof(path));
}
// if the path is empty, we want to return the original string instead of a single trailing character.
if (path.Length == 0 || path[path.Length - 1] == trailingCharacter)
{
return path;
}
return path + trailingCharacter;
}
public static string EnsureNoTrailingDirectorySeparator(string path)
{
if (!string.IsNullOrEmpty(path))
{
char lastChar = path[path.Length - 1];
if (lastChar == Path.DirectorySeparatorChar)
{
path = path.Substring(0, path.Length - 1);
}
}
return path;
}
public static void EnsureParentDirectoryExists(string filePath)
{
string directory = Path.GetDirectoryName(filePath);
EnsureDirectoryExists(directory);
}
public static void EnsureDirectoryExists(string directoryPath)
{
if (!Directory.Exists(directoryPath))
{
Directory.CreateDirectory(directoryPath);
}
}
public static bool TryDeleteDirectory(string directoryPath)
{
try
{
Directory.Delete(directoryPath, true);
return true;
}
catch
{
return false;
}
}
/// <summary>
/// Returns childItem relative to directory, with Path.DirectorySeparatorChar as separator
/// </summary>
public static string GetRelativePath(DirectoryInfo directory, FileSystemInfo childItem)
{
var path1 = EnsureTrailingSlash(directory.FullName);
var path2 = childItem.FullName;
return GetRelativePath(path1, path2, Path.DirectorySeparatorChar, true);
}
/// <summary>
/// Returns path2 relative to path1, with Path.DirectorySeparatorChar as separator
/// </summary>
public static string GetRelativePath(string path1, string path2)
{
if (!Path.IsPathRooted(path1) || !Path.IsPathRooted(path2))
{
throw new ArgumentException("both paths need to be rooted/full path");
}
return GetRelativePath(path1, path2, Path.DirectorySeparatorChar, true);
}
/// <summary>
/// Returns path2 relative to path1, with Path.DirectorySeparatorChar as separator but ignoring directory
/// traversals.
/// </summary>
public static string GetRelativePathIgnoringDirectoryTraversals(string path1, string path2)
{
return GetRelativePath(path1, path2, Path.DirectorySeparatorChar, false);
}
/// <summary>
/// Returns path2 relative to path1, with given path separator
/// </summary>
public static string GetRelativePath(string path1, string path2, char separator, bool includeDirectoryTraversals)
{
if (string.IsNullOrEmpty(path1))
{
throw new ArgumentException("Path must have a value", nameof(path1));
}
if (string.IsNullOrEmpty(path2))
{
throw new ArgumentException("Path must have a value", nameof(path2));
}
StringComparison compare;
if (RuntimeEnvironment.OperatingSystemPlatform == Platform.Windows)
{
compare = StringComparison.OrdinalIgnoreCase;
// check if paths are on the same volume
if (!string.Equals(Path.GetPathRoot(path1), Path.GetPathRoot(path2), compare))
{
// on different volumes, "relative" path is just path2
return path2;
}
}
else
{
compare = StringComparison.Ordinal;
}
var index = 0;
var path1Segments = path1.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
var path2Segments = path2.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
// if path1 does not end with / it is assumed the end is not a directory
// we will assume that is isn't a directory by ignoring the last split
var len1 = path1Segments.Length - 1;
var len2 = path2Segments.Length;
// find largest common absolute path between both paths
var min = Math.Min(len1, len2);
while (min > index)
{
if (!string.Equals(path1Segments[index], path2Segments[index], compare))
{
break;
}
// Handle scenarios where folder and file have same name (only if os supports same name for file and directory)
// e.g. /file/name /file/name/app
else if ((len1 == index && len2 > index + 1) || (len1 > index && len2 == index + 1))
{
break;
}
++index;
}
var path = "";
// check if path2 ends with a non-directory separator and if path1 has the same non-directory at the end
if (len1 + 1 == len2 && !string.IsNullOrEmpty(path1Segments[index]) &&
string.Equals(path1Segments[index], path2Segments[index], compare))
{
return path;
}
if (includeDirectoryTraversals)
{
for (var i = index; len1 > i; ++i)
{
path += ".." + separator;
}
}
for (var i = index; len2 - 1 > i; ++i)
{
path += path2Segments[i] + separator;
}
// if path2 doesn't end with an empty string it means it ended with a non-directory name, so we add it back
if (!string.IsNullOrEmpty(path2Segments[len2 - 1]))
{
path += path2Segments[len2 - 1];
}
return path;
}
public static string GetAbsolutePath(string basePath, string relativePath)
{
if (basePath == null)
{
throw new ArgumentNullException(nameof(basePath));
}
if (relativePath == null)
{
throw new ArgumentNullException(nameof(relativePath));
}
Uri resultUri = new Uri(new Uri(basePath), new Uri(relativePath, UriKind.Relative));
return resultUri.LocalPath;
}
public static string GetDirectoryName(string path)
{
path = path.TrimEnd(Path.DirectorySeparatorChar);
return path.Substring(Path.GetDirectoryName(path).Length).Trim(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
}
public static string GetPathWithForwardSlashes(string path)
{
return path.Replace('\\', '/');
}
public static string GetPathWithBackSlashes(string path)
{
return path.Replace('/', '\\');
}
public static string GetPathWithDirectorySeparator(string path)
{
if (Path.DirectorySeparatorChar == '/')
{
return GetPathWithForwardSlashes(path);
}
else
{
return GetPathWithBackSlashes(path);
}
}
public static string RemoveExtraPathSeparators(string path)
{
if (string.IsNullOrEmpty(path))
{
return path;
}
var components = path.Split(Path.DirectorySeparatorChar);
var result = string.Empty;
foreach (var component in components)
{
if (string.IsNullOrEmpty(component))
{
continue;
}
if (string.IsNullOrEmpty(result))
{
result = component;
// On Windows, manually append a separator for drive references because Path.Combine won't do so
if (result.EndsWith(":") && RuntimeEnvironment.OperatingSystemPlatform == Platform.Windows)
{
result += Path.DirectorySeparatorChar;
}
}
else
{
result = Path.Combine(result, component);
}
}
if (path[path.Length-1] == Path.DirectorySeparatorChar)
{
result += Path.DirectorySeparatorChar;
}
return result;
}
public static bool HasExtension(this string filePath, string extension)
{
var comparison = StringComparison.Ordinal;
if (RuntimeEnvironment.OperatingSystemPlatform == Platform.Windows)
{
comparison = StringComparison.OrdinalIgnoreCase;
}
return Path.GetExtension(filePath).Equals(extension, comparison);
}
/// <summary>
/// Gets the fully-qualified path without failing if the
/// path is empty.
/// </summary>
public static string GetFullPath(string path)
{
if (string.IsNullOrWhiteSpace(path))
{
return path;
}
return Path.GetFullPath(path);
}
public static void EnsureAllPathsExist(
IReadOnlyCollection<string> paths,
string pathDoesNotExistLocalizedFormatString,
bool allowDirectories = false)
{
var notExisting = new List<string>();
foreach (var p in paths)
{
if (!File.Exists(p) && (!allowDirectories || !Directory.Exists(p)))
{
notExisting.Add(p);
}
}
if (notExisting.Count > 0)
{
throw new GracefulException(
string.Join(
Environment.NewLine,
notExisting.Select(p => string.Format(pathDoesNotExistLocalizedFormatString, p))));
}
}
public static bool IsDirectory(this string path) =>
File.GetAttributes(path).HasFlag(FileAttributes.Directory);
}
}
// Usage:
//
// https://github.com/dotnet/cli/blob/95c6eff6daa1a69f29c42b2d405400ad44bdec91/src/Microsoft.DotNet.Cli.Sln.Internal/SlnFile.cs#L267-L279
private string _filePath;
public string FilePath
{
get
{
return _filePath;
}
set
{
_filePath = PathUtility.RemoveExtraPathSeparators(
PathUtility.GetPathWithDirectorySeparator(value));
}
}