File: PackageCreation\Authoring\ManifestReader.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.Xml;
using System.Xml.Linq;
using NuGet.Common;
using NuGet.Frameworks;
using NuGet.Packaging.Core;
using NuGet.Packaging.Licenses;
using NuGet.Packaging.PackageCreation.Resources;
using NuGet.Versioning;

namespace NuGet.Packaging
{
    internal static class ManifestReader
    {
        private static readonly string[] RequiredElements = new string[] { "id", "version", "authors", "description" };

        public static Manifest ReadManifest(XDocument document)
        {
            var metadataElement = document.Root!.ElementsNoNamespace("metadata").FirstOrDefault();
            if (metadataElement == null)
            {
                throw new InvalidDataException(
                    string.Format(CultureInfo.CurrentCulture, NuGetResources.Manifest_RequiredElementMissing, "metadata"));
            }

            return new Manifest(
                ReadMetadata(metadataElement),
                ReadFilesList(document.Root!.ElementsNoNamespace("files").FirstOrDefault()));
        }

        private static ManifestMetadata ReadMetadata(XElement xElement)
        {
            var manifestMetadata = new ManifestMetadata();
            manifestMetadata.MinClientVersionString = (string?)xElement.Attribute("minClientVersion");

            // we store all child elements under <metadata> so that we can easily check for required elements.
            var allElements = new HashSet<string>();

            foreach (var element in xElement.Elements())
            {
                ReadMetadataValue(manifestMetadata, element, allElements);
            }

            manifestMetadata.PackageTypes = NuspecUtility.GetPackageTypes(xElement, useMetadataNamespace: false);

            // now check for required elements, which include <id>, <version>, <authors> and <description>
            foreach (var requiredElement in RequiredElements)
            {
                if (requiredElement.Equals("authors", StringComparison.Ordinal) && manifestMetadata.PackageTypes.Contains(PackageType.SymbolsPackage))
                {
                    continue;
                }
                if (!allElements.Contains(requiredElement))
                {
                    throw new InvalidDataException(
                        string.Format(CultureInfo.CurrentCulture, NuGetResources.Manifest_RequiredElementMissing, requiredElement));
                }
            }

            return manifestMetadata;
        }

        private static void ReadMetadataValue(ManifestMetadata manifestMetadata, XElement element, HashSet<string> allElements)
        {
            if (element.Value == null)
            {
                return;
            }

            allElements.Add(element.Name.LocalName);

            string? value = null;
            try
            {
                value = element.Value?.Trim();
                switch (element.Name.LocalName)
                {
                    case "id":
                        manifestMetadata.Id = value;
                        break;
                    case "version":
                        if (NuGetVersion.TryParse(value, out NuGetVersion? version))
                        {
                            manifestMetadata.Version = version;
                        }
                        break;
                    case "authors":
                        manifestMetadata.Authors = value?.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)!;
                        break;
                    case "owners":
                        manifestMetadata.Owners = value?.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)!;
                        break;
                    case "licenseUrl":
                        manifestMetadata.SetLicenseUrl(value);
                        break;
                    case "projectUrl":
                        manifestMetadata.SetProjectUrl(value);
                        break;
                    case "iconUrl":
                        manifestMetadata.SetIconUrl(value);
                        break;
                    case "requireLicenseAcceptance":
                        manifestMetadata.RequireLicenseAcceptance = XmlConvert.ToBoolean(value!);
                        break;
                    case "developmentDependency":
                        manifestMetadata.DevelopmentDependency = XmlConvert.ToBoolean(value!);
                        break;
                    case "description":
                        manifestMetadata.Description = value;
                        break;
                    case "summary":
                        manifestMetadata.Summary = value;
                        break;
                    case "releaseNotes":
                        manifestMetadata.ReleaseNotes = value;
                        break;
                    case "copyright":
                        manifestMetadata.Copyright = value;
                        break;
                    case "language":
                        manifestMetadata.Language = value;
                        break;
                    case "title":
                        manifestMetadata.Title = value;
                        break;
                    case "tags":
                        manifestMetadata.Tags = value;
                        break;
                    case "readme":
                        manifestMetadata.Readme = value;
                        break;
                    case "serviceable":
                        manifestMetadata.Serviceable = XmlConvert.ToBoolean(value!);
                        break;
                    case "dependencies":
                        manifestMetadata.DependencyGroups = ReadDependencyGroups(element);
                        break;
                    case "frameworkAssemblies":
                        manifestMetadata.FrameworkReferences = ReadFrameworkAssemblies(element);
                        break;
                    case "frameworkReferences":
                        manifestMetadata.FrameworkReferenceGroups = ReadFrameworkReferenceGroups(element);
                        break;
                    case "references":
                        manifestMetadata.PackageAssemblyReferences = ReadReferenceSets(element);
                        break;
                    case "contentFiles":
                        manifestMetadata.ContentFiles = ReadContentFiles(element);
                        break;
                    case "repository":
                        manifestMetadata.Repository = ReadRepository(element);
                        break;
                    case "license":
                        manifestMetadata.LicenseMetadata = ReadLicenseMetadata(element);
                        break;
                    case "icon":
                        manifestMetadata.Icon = value;
                        break;
                }
            }
            catch (Exception ex) when (!(ex is InvalidDataException))
            {
                // Wrap the exception to pinpoint the exact property that is problematic,
                // and include a hint about replacement tokens.
                throw new InvalidDataException(string.Format(CultureInfo.CurrentCulture, NuGetResources.Manifest_PropertyValueReadFailure, value, element.Name.LocalName), ex);
            }
        }

        private static IEnumerable<FrameworkReferenceGroup> ReadFrameworkReferenceGroups(XElement frameworkReferenceGroupsElement)
        {
            return NuspecUtility.GetFrameworkReferenceGroups(frameworkReferenceGroupsElement, DefaultFrameworkNameProvider.Instance, useMetadataNamespace: false);
        }

        private static LicenseMetadata ReadLicenseMetadata(XElement licenseNode)
        {
            var type = licenseNode.Attribute(NuspecUtility.Type)!.Value?.Trim();
            var license = licenseNode.Value?.Trim();
            var versionValue = licenseNode.Attribute(NuspecUtility.Version)?.Value?.Trim();

            if (!Enum.TryParse(type, ignoreCase: true, result: out LicenseType licenseType))
            {
                throw new InvalidDataException(string.Format(CultureInfo.CurrentCulture, Strings.NuGetLicense_InvalidLicenseType, type));
            }

            if (string.IsNullOrWhiteSpace(license))
            {
                throw new InvalidDataException(string.Format(CultureInfo.CurrentCulture, Strings.NuGetLicense_MissingRequiredValue));
            }

            Version? version = null;
            if (string.IsNullOrWhiteSpace(versionValue))
            {
                version = LicenseMetadata.EmptyVersion;
            }
            else
            {
                if (!Version.TryParse(versionValue, out version!))
                {
                    throw new PackagingException(NuGetLogCode.NU5034, string.Format(
                        CultureInfo.CurrentCulture,
                        Strings.NuGetLicense_InvalidLicenseExpressionVersion,
                        versionValue));
                }
            }

            switch (licenseType)
            {
                case LicenseType.Expression:

                    if (version.CompareTo(LicenseMetadata.CurrentVersion) <= 0)
                    {
                        try
                        {
                            var expression = NuGetLicenseExpression.Parse(license!);
                            return new LicenseMetadata(licenseType, license!, expression, warningsAndErrors: null, version: version);
                        }
                        catch (NuGetLicenseExpressionParsingException e)
                        {
                            throw new PackagingException(NuGetLogCode.NU5032, e.Message);
                        }

                    }
                    throw new PackagingException(NuGetLogCode.NU5034, string.Format(
                                   CultureInfo.CurrentCulture,
                                   Strings.InvalidLicenseExppressionVersion_VersionTooHigh,
                                   versionValue,
                                   LicenseMetadata.CurrentVersion));

                case LicenseType.File:
                    return new LicenseMetadata(type: licenseType, license: license!, expression: null, warningsAndErrors: null, version: LicenseMetadata.EmptyVersion);

                default:
                    throw new InvalidDataException(string.Format(CultureInfo.CurrentCulture, Strings.NuGetLicense_InvalidLicenseType, type));
            }
        }

        private static List<ManifestContentFiles> ReadContentFiles(XElement contentFilesElement)
        {
            if (!contentFilesElement.HasElements)
            {
                return new List<ManifestContentFiles>(0);
            }

            var contentFileSets = (from element in contentFilesElement.ElementsNoNamespace("files")
                                   let includeAttribute = element.Attribute("include")
                                   where includeAttribute != null && !string.IsNullOrEmpty(includeAttribute.Value)
                                   let excludeAttribute = element.Attribute("exclude")
                                   let buildActionAttribute = element.Attribute("buildAction")
                                   let copyToOutputAttribute = element.Attribute("copyToOutput")
                                   let flattenAttribute = element.Attribute("flatten")
                                   select new ManifestContentFiles
                                   {
                                       Include = includeAttribute.Value.Trim(),
                                       Exclude = excludeAttribute == null ? null : excludeAttribute.Value,
                                       BuildAction = buildActionAttribute == null ? null : buildActionAttribute.Value,
                                       CopyToOutput = copyToOutputAttribute == null ? null : copyToOutputAttribute.Value,
                                       Flatten = flattenAttribute == null ? null : flattenAttribute.Value
                                   }).ToList();

            return contentFileSets;
        }

        private static List<PackageReferenceSet> ReadReferenceSets(XElement referencesElement)
        {
            if (!referencesElement.HasElements)
            {
                return new List<PackageReferenceSet>(0);
            }

            if (referencesElement.ElementsNoNamespace("group").Any() &&
                referencesElement.ElementsNoNamespace("reference").Any())
            {
                throw new InvalidDataException(NuGetResources.Manifest_ReferencesHasMixedElements);
            }

            var references = ReadReference(referencesElement, throwIfEmpty: false);
            if (references.Count > 0)
            {
                // old format, <reference> is direct child of <references>
                var referenceSet = new PackageReferenceSet(references);
                return new List<PackageReferenceSet> { referenceSet };
            }
            else
            {
                var groups = referencesElement.ElementsNoNamespace("group");
                return (from element in groups
                        select new PackageReferenceSet(element.GetOptionalAttributeValue("targetFramework")?.Trim(),
                            ReadReference(element, throwIfEmpty: true))).ToList();
            }
        }

        public static List<string> ReadReference(XElement referenceElement, bool throwIfEmpty)
        {
            var references = referenceElement.ElementsNoNamespace("reference")
                                             .Select(element => ((string?)element.Attribute("file"))?.Trim())
                                             .Where(file => file != null)
                                             .Select(file => file!)
                                             .ToList();

            if (throwIfEmpty && references.Count == 0)
            {
                throw new InvalidDataException(NuGetResources.Manifest_ReferencesIsEmpty);
            }

            return references;
        }

        private static List<FrameworkAssemblyReference> ReadFrameworkAssemblies(XElement frameworkElement)
        {
            if (!frameworkElement.HasElements)
            {
                return new List<FrameworkAssemblyReference>(0);
            }

            return (from element in frameworkElement.ElementsNoNamespace("frameworkAssembly")
                    let assemblyNameAttribute = element.Attribute("assemblyName")
                    where assemblyNameAttribute != null && !string.IsNullOrEmpty(assemblyNameAttribute.Value)
                    select new FrameworkAssemblyReference(assemblyNameAttribute.Value!.Trim(),
                        string.IsNullOrEmpty(element.GetOptionalAttributeValue("targetFramework")) ?
                        new[] { NuGetFramework.AnyFramework } :
                        new[] { NuGetFramework.Parse(element.GetOptionalAttributeValue("targetFramework")!.Trim()) })
                    ).ToList();
        }

        private static List<PackageDependencyGroup> ReadDependencyGroups(XElement dependenciesElement)
        {
            if (!dependenciesElement.HasElements)
            {
                return new List<PackageDependencyGroup>();
            }

            // Disallow the <dependencies> element to contain both <dependency> and 
            // <group> child elements. Unfortunately, this cannot be enforced by XSD.
            if (dependenciesElement.ElementsNoNamespace("dependency").Any() &&
                dependenciesElement.ElementsNoNamespace("group").Any())
            {
                throw new InvalidDataException(NuGetResources.Manifest_DependenciesHasMixedElements);
            }

            var dependencies = ReadDependencies(dependenciesElement);
            if (dependencies.Any())
            {
                // old format, <dependency> is direct child of <dependencies>
                var dependencyGroup = new PackageDependencyGroup(NuGetFramework.AnyFramework, dependencies);
                return new List<PackageDependencyGroup> { dependencyGroup };
            }
            else
            {
                var groups = dependenciesElement.ElementsNoNamespace("group");

                return groups.Select(element =>
                {
                    var targetFrameworkName = element.GetOptionalAttributeValue("targetFramework")?.Trim();
                    NuGetFramework? targetFramework = null;

                    if (targetFrameworkName != null)
                    {
                        targetFramework = NuGetFramework.Parse(targetFrameworkName);

                        if (targetFramework.IsUnsupported)
                        {
                            throw new InvalidDataException(string.Format(CultureInfo.CurrentCulture, Strings.Error_InvalidTargetFramework, targetFrameworkName));
                        }
                    }

                    // REVIEW: Is UnsupportedFramework correct?
                    targetFramework = targetFramework ?? NuGetFramework.UnsupportedFramework;

                    return new PackageDependencyGroup(
                            targetFramework,
                            ReadDependencies(element));
                }).ToList();
            }
        }

        private static ISet<PackageDependency> ReadDependencies(XElement containerElement)
        {
            // element is <dependency>

            var dependency = (from element in containerElement.ElementsNoNamespace("dependency")
                              let idElement = element.Attribute("id")
                              where idElement != null && !string.IsNullOrEmpty(idElement.Value)
                              let elementVersion = element.GetOptionalAttributeValue("version")
                              select new PackageDependency(
                                  idElement.Value!.Trim(),
                                  // REVIEW: There isn't a PackageDependency constructor that allows me to pass in an invalid version
                                  elementVersion == null ? null : VersionRange.Parse(elementVersion.Trim()),
                                  element.GetOptionalAttributeValue("include")?.Trim()?.Split(',').Select(a => a.Trim()).ToArray(),
                                  element.GetOptionalAttributeValue("exclude")?.Trim()?.Split(',').Select(a => a.Trim()).ToArray()
                              )).ToList();
            return new HashSet<PackageDependency>(dependency);
        }

        private static List<ManifestFile>? ReadFilesList(XElement? xElement)
        {
            if (xElement == null)
            {
                return null;
            }

            var files = new List<ManifestFile>();
            foreach (var file in xElement.ElementsNoNamespace("file"))
            {
                var srcElement = file.Attribute("src");
                if (srcElement == null || string.IsNullOrEmpty(srcElement.Value))
                {
                    continue;
                }

                var slashes = new[] { '\\', '/' };
                var target = file.GetOptionalAttributeValue("target")?.Trim()?.TrimStart(slashes);
                var exclude = file.GetOptionalAttributeValue("exclude")?.Trim();

                // Multiple sources can be specified by using semi-colon separated values. 
                files.AddRange(srcElement.Value.Trim(';').Split(';').Select(s =>
                    new ManifestFile
                    {
                        Source = s?.Trim(),
                        Target = target,
                        Exclude = exclude
                    }));
            }
            return files;
        }

        private static RepositoryMetadata? ReadRepository(XElement element)
        {
            var repositoryType = element.Attribute("type");
            var repositoryUrl = element.Attribute("url");
            var repositoryBranch = element.Attribute("branch");
            var repositoryCommit = element.Attribute("commit");
            var repository = new RepositoryMetadata();
            if (!string.IsNullOrEmpty(repositoryType?.Value))
            {
                repository.Type = repositoryType!.Value;
            }
            if (!string.IsNullOrEmpty(repositoryUrl?.Value))
            {
                repository.Url = repositoryUrl!.Value;
                repository.Branch = repositoryBranch?.Value ?? string.Empty;
                repository.Commit = repositoryCommit?.Value ?? string.Empty;
            }

            // Ensure the value is valid before returning it.
            if (!string.IsNullOrEmpty(repository.Type) && !string.IsNullOrEmpty(repository.Url))
            {
                return repository;
            }

            return null;
        }
    }
}