From 6e1c39d764c48667f55cdcf0fc6d5fb1914f76b5 Mon Sep 17 00:00:00 2001 From: "Eric St. John" Date: Tue, 12 Jan 2016 10:12:55 -0800 Subject: [PATCH] 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. --- .../NuGet/EmptyFrameworkFolderFile.cs | 26 ++ .../NuGet/Manifest.cs | 36 ++- .../NuGet/ManifestFile.cs | 21 ++ .../NuGet/ManifestMetadata.cs | 2 + .../NuGet/PackageBuilder.cs | 92 +++++- .../NuGet/PackageMetadataXmlExtensions.cs | 173 ++++++++++ .../NuGet/PackageReferenceSet.cs | 7 +- .../NuGet/PathResolver.cs | 304 ++++++++++++++++++ 8 files changed, 656 insertions(+), 5 deletions(-) create mode 100644 src/Microsoft.DotNet.Tools.Pack/NuGet/EmptyFrameworkFolderFile.cs create mode 100644 src/Microsoft.DotNet.Tools.Pack/NuGet/ManifestFile.cs create mode 100644 src/Microsoft.DotNet.Tools.Pack/NuGet/PathResolver.cs diff --git a/src/Microsoft.DotNet.Tools.Pack/NuGet/EmptyFrameworkFolderFile.cs b/src/Microsoft.DotNet.Tools.Pack/NuGet/EmptyFrameworkFolderFile.cs new file mode 100644 index 000000000..f183ff274 --- /dev/null +++ b/src/Microsoft.DotNet.Tools.Pack/NuGet/EmptyFrameworkFolderFile.cs @@ -0,0 +1,26 @@ +// 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.IO; + +namespace NuGet +{ + /// + /// Represents an empty framework folder in NuGet 2.0+ packages. + /// An empty framework folder is represented by a file named "_._". + /// + internal sealed class EmptyFrameworkFolderFile : PhysicalPackageFile + { + public EmptyFrameworkFolderFile(string directoryPathInPackage) : + base(() => Stream.Null) + { + if (directoryPathInPackage == null) + { + throw new ArgumentNullException("directoryPathInPackage"); + } + + TargetPath = System.IO.Path.Combine(directoryPathInPackage, Constants.PackageEmptyFileName); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.Tools.Pack/NuGet/Manifest.cs b/src/Microsoft.DotNet.Tools.Pack/NuGet/Manifest.cs index b1bcd0c1b..c15ef5972 100644 --- a/src/Microsoft.DotNet.Tools.Pack/NuGet/Manifest.cs +++ b/src/Microsoft.DotNet.Tools.Pack/NuGet/Manifest.cs @@ -2,6 +2,8 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; +using System.Collections; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Xml.Linq; @@ -24,6 +26,8 @@ namespace NuGet public ManifestMetadata Metadata { get; } + public ICollection Files { get; } = new List(); + /// /// Saves the current manifest to the specified stream. /// @@ -53,9 +57,37 @@ namespace NuGet int version = Math.Max(minimumManifestVersion, ManifestVersionUtility.GetManifestVersion(Metadata)); var schemaNamespace = (XNamespace)ManifestSchemaUtility.GetSchemaNamespace(version); - new XDocument( + var document = new XDocument( new XElement(schemaNamespace + "package", - Metadata.ToXElement(schemaNamespace))).Save(stream); + Metadata.ToXElement(schemaNamespace))); + + var fileElement = Files.ToXElement(schemaNamespace); + + if (fileElement != null) + { + document.Root.Add(fileElement); + } + + document.Save(stream); + } + + public static Manifest ReadFrom(Stream stream) + { + XDocument document = XDocument.Load(stream); + var schemaNamespace = GetSchemaNamespace(document); + + return document.Root.ReadManifest(schemaNamespace); + } + + private static string GetSchemaNamespace(XDocument document) + { + string schemaNamespace = ManifestSchemaUtility.SchemaVersionV1; + var rootNameSpace = document.Root.Name.Namespace; + if (rootNameSpace != null && !String.IsNullOrEmpty(rootNameSpace.NamespaceName)) + { + schemaNamespace = rootNameSpace.NamespaceName; + } + return schemaNamespace; } public static Manifest Create(PackageBuilder copy) diff --git a/src/Microsoft.DotNet.Tools.Pack/NuGet/ManifestFile.cs b/src/Microsoft.DotNet.Tools.Pack/NuGet/ManifestFile.cs new file mode 100644 index 000000000..bcb484aea --- /dev/null +++ b/src/Microsoft.DotNet.Tools.Pack/NuGet/ManifestFile.cs @@ -0,0 +1,21 @@ +// 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. + +namespace NuGet +{ + public class ManifestFile + { + public ManifestFile(string source, string target, string exclude) + { + Source = source; + Target = target; + Exclude = exclude; + } + + public string Source { get; } + + public string Target { get; } + + public string Exclude { get; } + } +} diff --git a/src/Microsoft.DotNet.Tools.Pack/NuGet/ManifestMetadata.cs b/src/Microsoft.DotNet.Tools.Pack/NuGet/ManifestMetadata.cs index f2dbe481e..3520387e0 100644 --- a/src/Microsoft.DotNet.Tools.Pack/NuGet/ManifestMetadata.cs +++ b/src/Microsoft.DotNet.Tools.Pack/NuGet/ManifestMetadata.cs @@ -61,6 +61,8 @@ namespace NuGet public bool RequireLicenseAcceptance { get; set; } + public bool DevelopmentDependency { get; set; } + public string Description { get; set; } public string Summary { get; set; } diff --git a/src/Microsoft.DotNet.Tools.Pack/NuGet/PackageBuilder.cs b/src/Microsoft.DotNet.Tools.Pack/NuGet/PackageBuilder.cs index 5a3f37981..f35aa9647 100644 --- a/src/Microsoft.DotNet.Tools.Pack/NuGet/PackageBuilder.cs +++ b/src/Microsoft.DotNet.Tools.Pack/NuGet/PackageBuilder.cs @@ -19,6 +19,7 @@ namespace NuGet { private const string DefaultContentType = "application/octet"; internal const string ManifestRelationType = "manifest"; + private bool _includeEmptyDirectories = false; public PackageBuilder() { @@ -264,6 +265,43 @@ namespace NuGet } } + public void Populate(ManifestMetadata manifestMetadata) + { + Id = manifestMetadata.Id; + Version = manifestMetadata.Version; + Title = manifestMetadata.Title; + AppendIfNotNull(Authors, manifestMetadata.Authors); + AppendIfNotNull(Owners, manifestMetadata.Owners); + IconUrl = manifestMetadata.IconUrl; + LicenseUrl = manifestMetadata.LicenseUrl; + ProjectUrl = manifestMetadata.ProjectUrl; + RequireLicenseAcceptance = manifestMetadata.RequireLicenseAcceptance; + DevelopmentDependency = manifestMetadata.DevelopmentDependency; + Description = manifestMetadata.Description; + Summary = manifestMetadata.Summary; + ReleaseNotes = manifestMetadata.ReleaseNotes; + Language = manifestMetadata.Language; + Copyright = manifestMetadata.Copyright; + MinClientVersion = manifestMetadata.MinClientVersion; + + if (manifestMetadata.Tags != null) + { + Tags.AddRange(ParseTags(manifestMetadata.Tags)); + } + + AppendIfNotNull(DependencySets, manifestMetadata.DependencySets); + AppendIfNotNull(FrameworkAssemblies, manifestMetadata.FrameworkAssemblies); + AppendIfNotNull(PackageAssemblyReferences, manifestMetadata.PackageAssemblyReferences); + } + + public void PopulateFiles(string basePath, IEnumerable files) + { + foreach (var file in files) + { + AddFiles(basePath, file.Source, file.Target, file.Exclude); + } + } + private void WriteManifest(ZipArchive package, int minimumManifestVersion) { string path = Id + Constants.ManifestExtension; @@ -310,6 +348,39 @@ namespace NuGet return extensions; } + private void AddFiles(string basePath, string source, string destination, string exclude = null) + { + List searchFiles = PathResolver.ResolveSearchPattern(basePath, source, destination, _includeEmptyDirectories).ToList(); + + ExcludeFiles(searchFiles, basePath, exclude); + + if (!PathResolver.IsWildcardSearch(source) && !PathResolver.IsDirectoryPath(source) && !searchFiles.Any()) + { + // TODO: Resources + throw new FileNotFoundException( + String.Format(CultureInfo.CurrentCulture, "NuGetResources.PackageAuthoring_FileNotFound {0}", source)); + } + + + Files.AddRange(searchFiles); + } + + private static void ExcludeFiles(List searchFiles, string basePath, string exclude) + { + if (String.IsNullOrEmpty(exclude)) + { + return; + } + + // One or more exclusions may be specified in the file. Split it and prepend the base path to the wildcard provided. + var exclusions = exclude.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries); + foreach (var item in exclusions) + { + string wildCard = PathResolver.NormalizeWildcardForExcludedFiles(basePath, item); + PathResolver.FilterPackageFiles(searchFiles, p => p.SourcePath, new[] { wildCard }); + } + } + private static void CreatePart(ZipArchive package, string path, Stream sourceStream) { if (PackageHelper.IsManifest(path)) @@ -324,6 +395,15 @@ namespace NuGet } } + /// + /// Tags come in this format. tag1 tag2 tag3 etc.. + /// + private static IEnumerable ParseTags(string tags) + { + return from tag in tags.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries) + select tag.Trim(); + } + private static bool IsPrereleaseDependency(PackageDependency dependency) { return dependency.VersionRange.MinVersion?.IsPrerelease == true || @@ -340,7 +420,7 @@ namespace NuGet return version == null || version.Release.Length <= 20; } - private void WriteOpcManifestRelationship(ZipArchive package, string path) + private static void WriteOpcManifestRelationship(ZipArchive package, string path) { ZipArchiveEntry relsEntry = package.CreateEntry("_rels/.rels", CompressionLevel.Optimal); @@ -374,9 +454,17 @@ namespace NuGet } // Generate a relationship id for compatibility - private string GenerateRelationshipId() + private static string GenerateRelationshipId() { return "R" + Guid.NewGuid().ToString("N").Substring(0, 16); } + + private static void AppendIfNotNull(List collection, IEnumerable toAdd) + { + if (toAdd != null) + { + collection.AddRange(toAdd); + } + } } } diff --git a/src/Microsoft.DotNet.Tools.Pack/NuGet/PackageMetadataXmlExtensions.cs b/src/Microsoft.DotNet.Tools.Pack/NuGet/PackageMetadataXmlExtensions.cs index 692029991..c78a50145 100644 --- a/src/Microsoft.DotNet.Tools.Pack/NuGet/PackageMetadataXmlExtensions.cs +++ b/src/Microsoft.DotNet.Tools.Pack/NuGet/PackageMetadataXmlExtensions.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Xml.Linq; using NuGet.Frameworks; using NuGet.Packaging.Core; +using NuGet.Versioning; namespace NuGet { @@ -21,6 +22,7 @@ namespace NuGet private const string FrameworkAssembly = "frameworkAssembly"; private const string AssemblyName = "assemblyName"; private const string Dependencies = "dependencies"; + private const string Files = "files"; public static XElement ToXElement(this ManifestMetadata metadata, XNamespace ns) { @@ -34,6 +36,7 @@ namespace NuGet elem.Add(new XElement(ns + "version", metadata.Version.ToString())); AddElementIfNotNull(elem, ns, "title", metadata.Title); elem.Add(new XElement(ns + "requireLicenseAcceptance", metadata.RequireLicenseAcceptance)); + elem.Add(new XElement(ns + "developmentDependency", metadata.DevelopmentDependency)); AddElementIfNotNull(elem, ns, "authors", metadata.Authors, authors => string.Join(",", authors)); AddElementIfNotNull(elem, ns, "owners", metadata.Owners, owners => string.Join(",", owners)); AddElementIfNotNull(elem, ns, "licenseUrl", metadata.LicenseUrl); @@ -71,6 +74,78 @@ namespace NuGet return elem; } + public static Manifest ReadManifest(this XElement element, XNamespace ns) + { + if (element.Name != ns + "package") + { + return null; + } + + var metadataElement = element.Element(ns + "metadata"); + if (metadataElement == null) + { + return null; + } + + ManifestMetadata metadata = new ManifestMetadata(); + + metadata.MinClientVersionString = metadataElement.Attribute("minClientVersion")?.Value; + metadata.Id = metadataElement.Element(ns + "id")?.Value; + metadata.Version = ConvertIfNotNull(metadataElement.Element(ns + "version")?.Value, s => new NuGetVersion(s)); + metadata.Title = metadataElement.Element(ns + "title")?.Value; + metadata.RequireLicenseAcceptance = ConvertIfNotNull(metadataElement.Element(ns + "requireLicenseAcceptance")?.Value, s => bool.Parse(s)); + metadata.DevelopmentDependency = ConvertIfNotNull(metadataElement.Element(ns + "developmentDependency")?.Value, s => bool.Parse(s)); + metadata.Authors = ConvertIfNotNull(metadataElement.Element(ns + "authors")?.Value, s => s.Split(',')); + metadata.Owners = ConvertIfNotNull(metadataElement.Element(ns + "owners")?.Value, s => s.Split(',')); + metadata.LicenseUrl = ConvertIfNotNull(metadataElement.Element(ns + "licenseUrl")?.Value, s => new Uri(s)); + metadata.ProjectUrl = ConvertIfNotNull(metadataElement.Element(ns + "projectUrl")?.Value, s => new Uri(s)); + metadata.IconUrl = ConvertIfNotNull(metadataElement.Element(ns + "iconUrl")?.Value, s => new Uri(s)); + metadata.Description = metadataElement.Element(ns + "description")?.Value; + metadata.Summary = metadataElement.Element(ns + "summary")?.Value; + metadata.ReleaseNotes = metadataElement.Element(ns + "releaseNotes")?.Value; + metadata.Copyright = metadataElement.Element(ns + "copyright")?.Value; + metadata.Language = metadataElement.Element(ns + "language")?.Value; + metadata.Tags = metadataElement.Element(ns + "tags")?.Value; + + metadata.DependencySets = GetItemSetsFromGroupableXElements( + ns, + metadataElement, + Dependencies, + "dependency", + TargetFramework, + GetPackageDependencyFromXElement, + (tfm, deps) => new PackageDependencySet(tfm, deps)); + + metadata.PackageAssemblyReferences = GetItemSetsFromGroupableXElements( + ns, + metadataElement, + References, + Reference, + TargetFramework, + GetPackageReferenceFromXElement, + (tfm, refs) => new PackageReferenceSet(tfm, refs)).ToArray(); + + metadata.FrameworkAssemblies = GetFrameworkAssembliesFromXElement(ns, metadataElement); + + Manifest manifest = new Manifest(metadata); + + var files = GetManifestFilesFromXElement(ns, element); + if (files != null) + { + foreach(var file in files) + { + manifest.Files.Add(file); + } + } + + return manifest; + } + + public static XElement ToXElement(this IEnumerable fileList, XNamespace ns) + { + return GetXElementFromManifestFiles(ns, fileList); + } + private static XElement GetXElementFromGroupableItemSets( XNamespace ns, IEnumerable objectSets, @@ -128,11 +203,45 @@ namespace NuGet return new XElement(ns + parentName, childElements.ToArray()); } + private static IEnumerable GetItemSetsFromGroupableXElements( + XNamespace ns, + XElement parent, + string rootName, + string elementName, + string identifierAttributeName, + Func getItemFromXElement, + Func, TSet> getItemSet) + { + XElement rootElement = parent.Element(ns + rootName); + + if (rootElement == null) + { + return Enumerable.Empty(); + } + + var groups = rootElement.Elements(ns + Group); + + if (groups == null || !groups.Any()) + { + // no groupable sets, all are ungroupable + return new[] { getItemSet(null, rootElement.Elements(ns + elementName).Select(e => getItemFromXElement(e))) }; + } + + return groups.Select(g => + getItemSet(g.Attribute(identifierAttributeName)?.Value, + g.Elements(ns + elementName).Select(e => getItemFromXElement(e)))); + } + private static XElement GetXElementFromPackageReference(XNamespace ns, string reference) { return new XElement(ns + Reference, new XAttribute(File, reference)); } + private static string GetPackageReferenceFromXElement(XElement element) + { + return element.Attribute(File)?.Value; + } + private static XElement GetXElementFromPackageDependency(XNamespace ns, PackageDependency dependency) { return new XElement(ns + "dependency", @@ -140,6 +249,12 @@ namespace NuGet dependency.VersionRange != null ? new XAttribute("version", dependency.VersionRange.ToString()) : null); } + private static PackageDependency GetPackageDependencyFromXElement(XElement element) + { + return new PackageDependency(element.Attribute("id").Value, + ConvertIfNotNull(element.Attribute("version")?.Value, s => VersionRange.Parse(s))); + } + private static XElement GetXElementFromFrameworkAssemblies(XNamespace ns, IEnumerable references) { if (references == null || !references.Any()) @@ -157,6 +272,53 @@ namespace NuGet null))); } + private static IEnumerable GetFrameworkAssembliesFromXElement(XNamespace ns, XElement parent) + { + var frameworkAssembliesElement = parent.Element(ns + FrameworkAssemblies); + + if (frameworkAssembliesElement == null) + { + return null; + } + + return frameworkAssembliesElement.Elements(ns + FrameworkAssembly).Select(e => + new FrameworkAssemblyReference(e.Attribute(AssemblyName).Value, + e.Attribute("targetFramework")?.Value?.Split(',')? + .Select(tf => NuGetFramework.Parse(tf)) ?? Enumerable.Empty())); + + } + + private static XElement GetXElementFromManifestFiles(XNamespace ns, IEnumerable files) + { + if (files == null || !files.Any()) + { + return null; + } + + return new XElement(ns + Files, + files.Select(file => + new XElement(ns + File, + new XAttribute("src", file.Source), + new XAttribute("target", file.Source), + new XAttribute("exclude", file.Exclude) + ))); + } + + private static IEnumerable GetManifestFilesFromXElement(XNamespace ns, XElement parent) + { + var filesElement = parent.Element(ns + Files); + + if (filesElement == null) + { + return null; + } + + return filesElement.Elements(ns + File).Select(f => + new ManifestFile(f.Attribute("src").Value, + f.Attribute("target").Value, + f.Attribute("exclude").Value)); + } + private static void AddElementIfNotNull(XElement parent, XNamespace ns, string name, T value) where T : class { @@ -178,5 +340,16 @@ namespace NuGet } } } + private static TDest ConvertIfNotNull(TSource value, Func convert) + { + if (value != null) + { + var converted = convert(value); + + return converted; + } + + return default(TDest); + } } } diff --git a/src/Microsoft.DotNet.Tools.Pack/NuGet/PackageReferenceSet.cs b/src/Microsoft.DotNet.Tools.Pack/NuGet/PackageReferenceSet.cs index 16515f0d8..6c2d228b9 100644 --- a/src/Microsoft.DotNet.Tools.Pack/NuGet/PackageReferenceSet.cs +++ b/src/Microsoft.DotNet.Tools.Pack/NuGet/PackageReferenceSet.cs @@ -11,7 +11,12 @@ namespace NuGet public class PackageReferenceSet { public PackageReferenceSet(IEnumerable references) - : this(null, references) + : this((NuGetFramework)null, references) + { + } + + public PackageReferenceSet(string targetFramework, IEnumerable references) + : this(targetFramework != null ? NuGetFramework.Parse(targetFramework) : null, references) { } diff --git a/src/Microsoft.DotNet.Tools.Pack/NuGet/PathResolver.cs b/src/Microsoft.DotNet.Tools.Pack/NuGet/PathResolver.cs new file mode 100644 index 000000000..af430dff3 --- /dev/null +++ b/src/Microsoft.DotNet.Tools.Pack/NuGet/PathResolver.cs @@ -0,0 +1,304 @@ +// 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 + { + /// + /// Returns a collection of files from the source that matches the wildcard. + /// + /// The collection of files to match. + /// Function that returns the path to filter a package file + /// The wildcards to apply to match the path with. + /// + public static IEnumerable GetMatches(IEnumerable source, Func getPath, IEnumerable wildcards) + { + var filters = wildcards.Select(WildcardToRegex); + return source.Where(item => + { + string path = getPath(item); + return filters.Any(f => f.IsMatch(path)); + }); + } + + /// + /// Removes files from the source that match any wildcard. + /// + public static void FilterPackageFiles(ICollection source, Func getPath, IEnumerable wildcards) + { + var matchedFiles = new HashSet(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 ResolveSearchPattern(string basePath, string searchPath, string targetPath, bool includeEmptyDirectories) + { + string normalizedBasePath; + IEnumerable 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 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 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; + } + + /// + /// 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. + /// + 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. --> 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); + } + + /// + /// Returns true if the path contains any wildcard characters. + /// + 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; + } + } + } +} \ No newline at end of file