File: PackageFolderReader.cs
Web Access
Project: src\src\nuget-client\src\NuGet.Core\NuGet.Packaging\NuGet.Packaging.csproj (NuGet.Packaging)
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using NuGet.Common;
using NuGet.Frameworks;
using NuGet.Packaging.Core;
using NuGet.Packaging.Signing;

namespace NuGet.Packaging
{
    /// <summary>
    /// Reads an unzipped nupkg folder.
    /// </summary>
    public class PackageFolderReader : PackageReaderBase
    {
        private readonly DirectoryInfo _root;

        /// <summary>
        /// Package folder reader
        /// </summary>
        public PackageFolderReader(string folderPath)
            : this(folderPath, DefaultFrameworkNameProvider.Instance, DefaultCompatibilityProvider.Instance)
        {
        }

        /// <summary>
        /// Package folder reader
        /// </summary>
        /// <param name="folder">root directory of an extracted nupkg</param>
        public PackageFolderReader(DirectoryInfo folder)
            : this(folder, DefaultFrameworkNameProvider.Instance, DefaultCompatibilityProvider.Instance)
        {
        }

        /// <summary>
        /// Package folder reader
        /// </summary>
        /// <param name="folderPath">root directory of an extracted nupkg</param>
        /// <param name="frameworkProvider">framework mappings</param>
        /// <param name="compatibilityProvider">framework compatibility provider</param>
        public PackageFolderReader(string folderPath, IFrameworkNameProvider frameworkProvider, IFrameworkCompatibilityProvider compatibilityProvider)
            : this(new DirectoryInfo(folderPath), frameworkProvider, compatibilityProvider)
        {
        }

        /// <summary>
        /// Package folder reader
        /// </summary>
        /// <param name="folder">root directory of an extracted nupkg</param>
        /// <param name="frameworkProvider">framework mappings</param>
        /// <param name="compatibilityProvider">framework compatibility provider</param>
        public PackageFolderReader(DirectoryInfo folder, IFrameworkNameProvider frameworkProvider, IFrameworkCompatibilityProvider compatibilityProvider)
            : base(frameworkProvider, compatibilityProvider)
        {
            _root = folder;
        }

        public override string GetNuspecFile()
        {
            // This needs to be explicitly case insensitive in order to work on XPlat, since GetFiles is normally case sensitive on non-Windows
            var nuspecFiles = _root.GetFiles("*.*", SearchOption.TopDirectoryOnly).Where(f => f.Name.EndsWith(".nuspec", StringComparison.OrdinalIgnoreCase)).ToArray();

            if (nuspecFiles.Length == 0)
            {
                var message = new StringBuilder();
                message.Append(Strings.Error_MissingNuspecFile);
                message.AppendFormat(CultureInfo.CurrentCulture, Strings.Message_Path, _root.FullName);
                throw new PackagingException(NuGetLogCode.NU5037, message.ToString());
            }
            else if (nuspecFiles.Length > 1)
            {
                throw new PackagingException(Strings.MultipleNuspecFiles);
            }

            return nuspecFiles[0].FullName;
        }

        /// <summary>
        /// Opens a local file in read only mode.
        /// </summary>
        public override Stream GetStream(string path)
        {
            return GetFile(path).OpenRead();
        }

        private FileInfo GetFile(string path)
        {
            var file = new FileInfo(Path.Combine(_root.FullName, path));

            if (!file.FullName.StartsWith(_root.FullName, StringComparison.OrdinalIgnoreCase))
            {
                // the given path does not appear under the folder root
                throw new FileNotFoundException(path);
            }

            return file;
        }

        public override IEnumerable<string> GetFiles()
        {
            // Read all files starting at the root.
            return GetFiles(folder: string.Empty);
        }

        public override IEnumerable<string> GetFiles(string folder)
        {
            // Default to retrieve files and throwing if the root
            // directory is not found.
            var getFiles = true;
            var searchFolder = new DirectoryInfo(_root.FullName);

            if (!string.IsNullOrEmpty(folder))
            {
                // Search in the sub folder if one was specified
                searchFolder = new DirectoryInfo(Path.Combine(_root.FullName, folder));

                // For sub folders verify it exists
                // The root is expected to exist and should throw if it does not
                getFiles = searchFolder.Exists;
            }

            if (getFiles)
            {
                // Enumerate root folder filtering out nupkg files
                foreach (var file in searchFolder.GetFiles("*", SearchOption.AllDirectories))
                {
                    var path = GetRelativePath(_root, file);

                    // disallow nupkgs in the root
                    if (!IsFileInRoot(path) || !IsNupkg(path))
                    {
                        yield return path;
                    }
                }
            }

            yield break;
        }

        /// <summary>
        /// True if the path does not contain /
        /// </summary>
        private static bool IsFileInRoot(string path)
        {
#if NETCOREAPP
            return path.IndexOf('/', StringComparison.Ordinal) == -1;
#else
            return path.IndexOf('/') == -1;
#endif
        }

        /// <summary>
        /// True if the path ends with .nupkg
        /// </summary>
        private static bool IsNupkg(string path)
        {
            return path.EndsWith(PackagingCoreConstants.NupkgExtension, StringComparison.OrdinalIgnoreCase) == true;
        }

        /// <summary>
        /// Build the relative path in the same format that ZipArchive uses
        /// </summary>
        private static string GetRelativePath(DirectoryInfo root, FileInfo file)
        {
            var parents = new Stack<DirectoryInfo>();

            var parent = file.Directory;

            while (parent != null
                   && !StringComparer.OrdinalIgnoreCase.Equals(parent.FullName, root.FullName))
            {
                parents.Push(parent);
                parent = parent.Parent;
            }

            if (parent == null)
            {
                // the given file path does not appear under root
                throw new FileNotFoundException(file.FullName);
            }

            var parts = parents.Select(d => d.Name).Concat(new string[] { file.Name });

            return string.Join("/", parts);
        }

        public override IEnumerable<string> CopyFiles(
            string destination,
            IEnumerable<string> packageFiles,
            ExtractPackageFileDelegate extractFile,
            ILogger logger,
            CancellationToken token)
        {
            var filesCopied = new List<string>();

            foreach (var packageFile in packageFiles)
            {
                token.ThrowIfCancellationRequested();

                var sourceFile = GetFile(packageFile);

                var targetPath = Path.Combine(destination, packageFile);
                Directory.CreateDirectory(Path.GetDirectoryName(targetPath)!);

                using (var fileStream = sourceFile.OpenRead())
                {
                    targetPath = extractFile(sourceFile.FullName, targetPath, fileStream);
                    if (targetPath != null)
                    {
                        ZipArchiveExtensions.UpdateFileTime(targetPath, sourceFile.LastWriteTimeUtc);
                        filesCopied.Add(targetPath);
                    }
                }
            }

            return filesCopied;
        }

        protected override void Dispose(bool disposing)
        {
            // do nothing here
        }

        public override Task<PrimarySignature?> GetPrimarySignatureAsync(CancellationToken token)
        {
            return TaskResult.Null<PrimarySignature>();
        }

        public override Task<bool> IsSignedAsync(CancellationToken token)
        {
            return TaskResult.False;
        }

        public override Task ValidateIntegrityAsync(SignatureContent signatureContent, CancellationToken token)
        {
            throw new NotImplementedException();
        }

        public override Task<byte[]> GetArchiveHashAsync(HashAlgorithmName hashAlgorithm, CancellationToken token)
        {
            throw new NotImplementedException();
        }

        public override bool CanVerifySignedPackages(SignedPackageVerifierSettings verifierSettings)
        {
            return false;
        }

        public override string GetContentHash(CancellationToken token, Func<string>? GetUnsignedPackageHash = null)
        {
            throw new NotImplementedException();
        }
    }
}