File: Utility\OfflineFeedUtility.cs
Web Access
Project: src\src\nuget-client\src\NuGet.Core\NuGet.Protocol\NuGet.Protocol.csproj (NuGet.Protocol)
// 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.Globalization;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
using NuGet.Common;
using NuGet.Packaging;
using NuGet.Packaging.Core;

namespace NuGet.Protocol.Core.Types
{
    public static class OfflineFeedUtility
    {
        public static bool PackageExists(
            PackageIdentity packageIdentity,
            string offlineFeed,
            out bool isValidPackage)
        {
            if (packageIdentity == null)
            {
                throw new ArgumentNullException(nameof(packageIdentity));
            }

            if (string.IsNullOrEmpty(offlineFeed))
            {
                throw new ArgumentNullException(nameof(offlineFeed));
            }

            var versionFolderPathResolver = new VersionFolderPathResolver(offlineFeed);
            var nupkgFilePath = versionFolderPathResolver.GetPackageFilePath(packageIdentity.Id, packageIdentity.Version);
            var hashFilePath = versionFolderPathResolver.GetHashPath(packageIdentity.Id, packageIdentity.Version);
            var nuspecFilePath = versionFolderPathResolver.GetManifestFilePath(packageIdentity.Id, packageIdentity.Version);

            var nupkgFileExists = File.Exists(nupkgFilePath);

            var hashFileExists = File.Exists(hashFilePath);

            var nuspecFileExists = File.Exists(nuspecFilePath);

            if (nupkgFileExists || hashFileExists || nuspecFileExists)
            {
                if (!nupkgFileExists || !hashFileExists || !nuspecFileExists)
                {
                    // One of the necessary files to represent the package in the feed does not exist
                    isValidPackage = false;
                }
                else
                {
                    // All the necessary files to represent the package in the feed are present.
                    // Check if the existing nupkg matches the hash. Otherwise, it is considered invalid.
                    var packageHash = GetHash(nupkgFilePath);
                    var existingHash = File.ReadAllText(hashFilePath);

                    isValidPackage = packageHash.Equals(existingHash, StringComparison.Ordinal);
                }

                return true;
            }

            isValidPackage = false;
            return false;
        }

        public static string? GetPackageDirectory(PackageIdentity packageIdentity, string offlineFeed)
        {
            var versionFolderPathResolver = new VersionFolderPathResolver(offlineFeed);
            return Path.GetDirectoryName(
                versionFolderPathResolver.GetPackageFilePath(packageIdentity.Id, packageIdentity.Version));
        }

        public static void ThrowIfInvalid(string path)
        {
            var pathUri = UriUtility.TryCreateSourceUri(path, UriKind.RelativeOrAbsolute);
            if (pathUri == null)
            {
                throw new ArgumentException(string.Format(CultureInfo.CurrentCulture,
                    Strings.Path_Invalid,
                    path));
            }

            var invalidPathChars = Path.GetInvalidPathChars();
#if NETCOREAPP
            if (invalidPathChars.Any(p => path.Contains(p, StringComparison.Ordinal)))
#else
            if (invalidPathChars.Any(p => path.Contains(p)))
#endif
            {
                throw new ArgumentException(string.Format(CultureInfo.CurrentCulture,
                    Strings.Path_Invalid,
                    path));
            }

            if (!pathUri.IsAbsoluteUri)
            {
                path = Path.GetFullPath(path);
                pathUri = new Uri(path);
            }

            if (!pathUri.IsFile && !pathUri.IsUnc)
            {
                throw new ArgumentException(string.Format(CultureInfo.CurrentCulture,
                    Strings.Path_Invalid_NotFileNotUnc,
                    path));
            }
        }

        public static void ThrowIfInvalidOrNotFound(
            string path,
            bool isDirectory,
            string resourceString)
        {
            if (resourceString == null)
            {
                throw new ArgumentNullException(nameof(resourceString));
            }

            ThrowIfInvalid(path);

            if ((isDirectory && !Directory.Exists(path)) ||
                (!isDirectory && !File.Exists(path)))
            {
                throw new ArgumentException(string.Format(CultureInfo.CurrentCulture,
                    resourceString,
                    path));
            }
        }

        public static async Task AddPackageToSource(
            OfflineFeedAddContext offlineFeedAddContext,
            CancellationToken token)
        {
            if (offlineFeedAddContext == null)
            {
                throw new ArgumentNullException(nameof(offlineFeedAddContext));
            }

            token.ThrowIfCancellationRequested();

            var packagePath = offlineFeedAddContext.PackagePath;
            var source = offlineFeedAddContext.Source;
            var logger = offlineFeedAddContext.Logger;

            using var packageStream = File.OpenRead(packagePath);
            try
            {
                var packageIdentity = default(PackageIdentity);
                using var packageReader = new PackageArchiveReader(packageStream, leaveStreamOpen: true);

                packageIdentity = packageReader.GetIdentity();


                bool isValidPackage;
                if (PackageExists(packageIdentity, source, out isValidPackage))
                {
                    // Package already exists. Verify if it is valid
                    if (isValidPackage)
                    {
                        var message = string.Format(
                            CultureInfo.CurrentCulture,
                            Strings.AddPackage_PackageAlreadyExists,
                            packageIdentity,
                            source);

                        if (offlineFeedAddContext.ThrowIfPackageExists)
                        {
                            throw new ArgumentException(message);
                        }
                        else
                        {
                            logger.LogMinimal(message);
                        }
                    }
                    else
                    {
                        var message = string.Format(CultureInfo.CurrentCulture,
                            Strings.AddPackage_ExistingPackageInvalid,
                            packageIdentity,
                            source);

                        if (offlineFeedAddContext.ThrowIfPackageExistsAndInvalid)
                        {
                            throw new ArgumentException(message);
                        }
                        else
                        {
                            logger.LogWarning(message);
                        }
                    }
                }
                else
                {
                    var versionFolderPathResolver = new VersionFolderPathResolver(source);

                    using var packageDownloader = new LocalPackageArchiveDownloader(
                        source: null,
                        packageFilePath: packagePath,
                        packageIdentity: packageIdentity,
                        logger: logger);

                    // Set Empty parentId here.
                    await PackageExtractor.InstallFromSourceAsync(
                        packageIdentity,
                        packageDownloader,
                        versionFolderPathResolver,
                        offlineFeedAddContext.ExtractionContext,
                        token,
                        parentId: Guid.Empty);


                    var message = string.Format(
                        CultureInfo.CurrentCulture,
                        Strings.AddPackage_SuccessfullyAdded,
                        packagePath,
                        source);

                    logger.LogMinimal(message);
                }
            }
            // Mono will throw ArchiveException when package is invalid.
            // Reading Nuspec in invalid package on Mono will get PackagingException 
            catch (Exception ex) when (ex is InvalidDataException
                                    || (RuntimeEnvironmentHelper.IsMono
                                    && (ex.GetType().FullName?.Equals("SharpCompress.Common.ArchiveException", StringComparison.Ordinal) == true
                                    || ex is PackagingException)))
            {
                var message = string.Format(
                    CultureInfo.CurrentCulture,
                    Strings.NupkgPath_Invalid,
                    packagePath);

                if (offlineFeedAddContext.ThrowIfSourcePackageIsInvalid)
                {
                    throw new ArgumentException(message);
                }
                else
                {
                    logger.LogWarning(message);
                }
            }
        }

        private static string GetHash(string nupkgFilePath)
        {
            string packageHash;
            using var nupkgStream = File.Open(nupkgFilePath, FileMode.Open, FileAccess.Read, FileShare.Read);
            using var sha512 = SHA512.Create();

            packageHash = Convert.ToBase64String(sha512.ComputeHash(nupkgStream));

            return packageHash;
        }
    }
}