dotnet-installer/src/Microsoft.DotNet.Tools.Pack/NuGet/PathResolver.cs
Eric St. John 6e1c39d764 Add support for NuSpecs
This brings over the NuGet.Core code that existed for
loading NuSpecs.  I also added the File list to the
manifest to support builing a package from a
nuspec, and the ability to save a nuspec.
2016-01-13 09:51:19 -08:00

304 lines
No EOL
14 KiB
C#

// 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 System.Text.RegularExpressions;
namespace NuGet
{
public static class PathResolver
{
/// <summary>
/// Returns a collection of files from the source that matches the wildcard.
/// </summary>
/// <param name="source">The collection of files to match.</param>
/// <param name="getPath">Function that returns the path to filter a package file </param>
/// <param name="wildcards">The wildcards to apply to match the path with.</param>
/// <returns></returns>
public static IEnumerable<T> GetMatches<T>(IEnumerable<T> source, Func<T, string> getPath, IEnumerable<string> wildcards)
{
var filters = wildcards.Select(WildcardToRegex);
return source.Where(item =>
{
string path = getPath(item);
return filters.Any(f => f.IsMatch(path));
});
}
/// <summary>
/// Removes files from the source that match any wildcard.
/// </summary>
public static void FilterPackageFiles<T>(ICollection<T> source, Func<T, string> getPath, IEnumerable<string> wildcards)
{
var matchedFiles = new HashSet<T>(GetMatches(source, getPath, wildcards));
var itemsToRemove = source.Where(matchedFiles.Contains).ToArray();
foreach (var item in itemsToRemove)
{
source.Remove(item);
}
}
public static string NormalizeWildcardForExcludedFiles(string basePath, string wildcard)
{
if (wildcard.StartsWith("**", StringComparison.OrdinalIgnoreCase))
{
// Allow any path to match the first '**' segment, see issue 2891 for more details.
return wildcard;
}
basePath = NormalizeBasePath(basePath, ref wildcard);
return Path.Combine(basePath, wildcard);
}
private static Regex WildcardToRegex(string wildcard)
{
var pattern = Regex.Escape(wildcard);
if (Path.DirectorySeparatorChar == '/')
{
// regex wildcard adjustments for *nix-style file systems
pattern = pattern
.Replace(@"\*\*/", ".*") //For recursive wildcards /**/, include the current directory.
.Replace(@"\*\*", ".*") // For recursive wildcards that don't end in a slash e.g. **.txt would be treated as a .txt file at any depth
.Replace(@"\*", @"[^/]*(/)?") // For non recursive searches, limit it any character that is not a directory separator
.Replace(@"\?", "."); // ? translates to a single any character
}
else
{
// regex wildcard adjustments for Windows-style file systems
pattern = pattern
.Replace("/", @"\\") // On Windows, / is treated the same as \.
.Replace(@"\*\*\\", ".*") //For recursive wildcards \**\, include the current directory.
.Replace(@"\*\*", ".*") // For recursive wildcards that don't end in a slash e.g. **.txt would be treated as a .txt file at any depth
.Replace(@"\*", @"[^\\]*(\\)?") // For non recursive searches, limit it any character that is not a directory separator
.Replace(@"\?", "."); // ? translates to a single any character
}
return new Regex('^' + pattern + '$', RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture);
}
internal static IEnumerable<PhysicalPackageFile> ResolveSearchPattern(string basePath, string searchPath, string targetPath, bool includeEmptyDirectories)
{
string normalizedBasePath;
IEnumerable<SearchPathResult> searchResults = PerformWildcardSearchInternal(basePath, searchPath, includeEmptyDirectories, out normalizedBasePath);
return searchResults.Select(result =>
result.IsFile
? new PhysicalPackageFile
{
SourcePath = result.Path,
TargetPath = ResolvePackagePath(normalizedBasePath, searchPath, result.Path, targetPath)
}
: new EmptyFrameworkFolderFile(ResolvePackagePath(normalizedBasePath, searchPath, result.Path, targetPath))
{
SourcePath = result.Path
}
);
}
public static IEnumerable<string> PerformWildcardSearch(string basePath, string searchPath)
{
string normalizedBasePath;
var searchResults = PerformWildcardSearchInternal(basePath, searchPath, includeEmptyDirectories: false, normalizedBasePath: out normalizedBasePath);
return searchResults.Select(s => s.Path);
}
private static IEnumerable<SearchPathResult> PerformWildcardSearchInternal(string basePath, string searchPath, bool includeEmptyDirectories, out string normalizedBasePath)
{
if (!searchPath.StartsWith(@"\\", StringComparison.OrdinalIgnoreCase)
&& Path.DirectorySeparatorChar != '/')
{
//If the system's DirectorySeparatorChar is '/' we're probably dealing with Mac or *nix
// In any case, if '/' is the separator, we don't want to trim off the first char ever
// since it would completely change the meaning of the path
// eg: /var/somedir/ is not at all the same as var/somedir (relative vs absolute)
// If we aren't dealing with network paths, trim the leading slash.
searchPath = searchPath.TrimStart(Path.DirectorySeparatorChar);
}
bool searchDirectory = false;
// If the searchPath ends with \ or /, we treat searchPath as a directory,
// and will include everything under it, recursively
if (IsDirectoryPath(searchPath))
{
searchPath = searchPath + "**" + Path.DirectorySeparatorChar + "*";
searchDirectory = true;
}
basePath = NormalizeBasePath(basePath, ref searchPath);
normalizedBasePath = GetPathToEnumerateFrom(basePath, searchPath);
// Append the basePath to searchPattern and get the search regex. We need to do this because the search regex is matched from line start.
Regex searchRegex = WildcardToRegex(Path.Combine(basePath, searchPath));
// This is a hack to prevent enumerating over the entire directory tree if the only wildcard characters are the ones in the file name.
// If the path portion of the search path does not contain any wildcard characters only iterate over the TopDirectory.
SearchOption searchOption = SearchOption.AllDirectories;
// (a) Path is not recursive search
bool isRecursiveSearch = searchPath.IndexOf("**", StringComparison.OrdinalIgnoreCase) != -1;
// (b) Path does not have any wildcards.
bool isWildcardPath = Path.GetDirectoryName(searchPath).Contains('*');
if (!isRecursiveSearch && !isWildcardPath)
{
searchOption = SearchOption.TopDirectoryOnly;
}
// Starting from the base path, enumerate over all files and match it using the wildcard expression provided by the user.
// Note: We use Directory.GetFiles() instead of Directory.EnumerateFiles() here to support Mono
var matchedFiles = from file in Directory.GetFiles(normalizedBasePath, "*.*", searchOption)
where searchRegex.IsMatch(file)
select new SearchPathResult(file, isFile: true);
if (!includeEmptyDirectories)
{
return matchedFiles;
}
// retrieve empty directories
// Note: We use Directory.GetDirectories() instead of Directory.EnumerateDirectories() here to support Mono
var matchedDirectories = from directory in Directory.GetDirectories(normalizedBasePath, "*.*", searchOption)
where searchRegex.IsMatch(directory) && IsEmptyDirectory(directory)
select new SearchPathResult(directory, isFile: false);
if (searchDirectory && IsEmptyDirectory(normalizedBasePath))
{
matchedDirectories = matchedDirectories.Concat(new [] { new SearchPathResult(normalizedBasePath, isFile: false) });
}
return matchedFiles.Concat(matchedDirectories);
}
internal static string GetPathToEnumerateFrom(string basePath, string searchPath)
{
string basePathToEnumerate;
int wildcardIndex = searchPath.IndexOf('*');
if (wildcardIndex == -1)
{
// For paths without wildcard, we could either have base relative paths (such as lib\foo.dll) or paths outside the base path
// (such as basePath: C:\packages and searchPath: D:\packages\foo.dll)
// In this case, Path.Combine would pick up the right root to enumerate from.
var searchRoot = Path.GetDirectoryName(searchPath);
basePathToEnumerate = Path.Combine(basePath, searchRoot);
}
else
{
// If not, find the first directory separator and use the path to the left of it as the base path to enumerate from.
int directorySeparatoryIndex = searchPath.LastIndexOf(Path.DirectorySeparatorChar, wildcardIndex);
if (directorySeparatoryIndex == -1)
{
// We're looking at a path like "NuGet*.dll", NuGet*\bin\release\*.dll
// In this case, the basePath would continue to be the path to begin enumeration from.
basePathToEnumerate = basePath;
}
else
{
string nonWildcardPortion = searchPath.Substring(0, directorySeparatoryIndex);
basePathToEnumerate = Path.Combine(basePath, nonWildcardPortion);
}
}
return basePathToEnumerate;
}
/// <summary>
/// Determins the path of the file inside a package.
/// For recursive wildcard paths, we preserve the path portion beginning with the wildcard.
/// For non-recursive wildcard paths, we use the file name from the actual file path on disk.
/// </summary>
internal static string ResolvePackagePath(string searchDirectory, string searchPattern, string fullPath, string targetPath)
{
string packagePath;
bool isDirectorySearch = IsDirectoryPath(searchPattern);
bool isWildcardSearch = IsWildcardSearch(searchPattern);
bool isRecursiveWildcardSearch = isWildcardSearch && searchPattern.IndexOf("**", StringComparison.OrdinalIgnoreCase) != -1;
if ((isRecursiveWildcardSearch || isDirectorySearch) && fullPath.StartsWith(searchDirectory, StringComparison.OrdinalIgnoreCase))
{
// The search pattern is recursive. Preserve the non-wildcard portion of the path.
// e.g. Search: X:\foo\**\*.cs results in SearchDirectory: X:\foo and a file path of X:\foo\bar\biz\boz.cs
// Truncating X:\foo\ would result in the package path.
packagePath = fullPath.Substring(searchDirectory.Length).TrimStart(Path.DirectorySeparatorChar);
}
else if (!isWildcardSearch && Path.GetExtension(searchPattern).Equals(Path.GetExtension(targetPath), StringComparison.OrdinalIgnoreCase))
{
// If the search does not contain wild cards, and the target path shares the same extension, copy it
// e.g. <file src="ie\css\style.css" target="Content\css\ie.css" /> --> Content\css\ie.css
return targetPath;
}
else
{
packagePath = Path.GetFileName(fullPath);
}
return Path.Combine(targetPath ?? String.Empty, packagePath);
}
private static readonly string OneDotSlash = "." + Path.DirectorySeparatorChar;
private static readonly string TwoDotSlash = ".." + Path.DirectorySeparatorChar;
internal static string NormalizeBasePath(string basePath, ref string searchPath)
{
// If no base path is provided, use the current directory.
basePath = String.IsNullOrEmpty(basePath) ? OneDotSlash : basePath;
// If the search path is relative, transfer the ..\ portion to the base path.
// This needs to be done because the base path determines the root for our enumeration.
while (searchPath.StartsWith(TwoDotSlash, StringComparison.OrdinalIgnoreCase))
{
basePath = Path.Combine(basePath, TwoDotSlash);
searchPath = searchPath.Substring(TwoDotSlash.Length);
}
return Path.GetFullPath(basePath);
}
/// <summary>
/// Returns true if the path contains any wildcard characters.
/// </summary>
internal static bool IsWildcardSearch(string filter)
{
return filter.IndexOf('*') != -1;
}
internal static bool IsDirectoryPath(string path)
{
return path != null && path.Length > 1 &&
(path[path.Length - 1] == Path.DirectorySeparatorChar ||
path[path.Length - 1] == Path.AltDirectorySeparatorChar);
}
private static bool IsEmptyDirectory(string directory)
{
return !Directory.EnumerateFileSystemEntries(directory).Any();
}
private struct SearchPathResult
{
private readonly string _path;
private readonly bool _isFile;
public string Path
{
get
{
return _path;
}
}
public bool IsFile
{
get
{
return _isFile;
}
}
public SearchPathResult(string path, bool isFile)
{
_path = path;
_isFile = isFile;
}
}
}
}