|
// 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.Collections.ObjectModel;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Xml.Linq;
using NuGet.Client;
using NuGet.Common;
using NuGet.Configuration;
using NuGet.ContentModel;
using NuGet.Frameworks;
using NuGet.Packaging.Core;
using NuGet.Packaging.PackageCreation.Resources;
using NuGet.RuntimeModel;
using NuGet.Versioning;
namespace NuGet.Packaging
{
public class PackageBuilder : IPackageMetadata
{
private static readonly Uri DefaultUri = new Uri("http://defaultcontainer/");
private static readonly DateTime ZipFormatMinDate = new DateTime(1980, 1, 1, 0, 0, 0, DateTimeKind.Utc);
private static readonly DateTime ZipFormatMaxDate = new DateTime(2107, 12, 31, 23, 59, 58, DateTimeKind.Utc);
internal const string ManifestRelationType = "manifest";
private readonly bool _includeEmptyDirectories;
private readonly bool _deterministic;
private readonly DateTimeOffset _deterministicTimestamp = DateTimeOffset.UtcNow;
private readonly ILogger _logger;
private readonly string? _versionOverride;
/// <summary>
/// Maximum Icon file size: 1 megabyte
/// </summary>
public const int MaxIconFileSize = 1024 * 1024;
public PackageBuilder(string path, Func<string, string>? propertyProvider, bool includeEmptyDirectories)
: this(path, propertyProvider, includeEmptyDirectories, deterministic: false)
{
}
public PackageBuilder(string path, Func<string, string>? propertyProvider, bool includeEmptyDirectories, bool deterministic)
: this(path, Path.GetDirectoryName(path)!, propertyProvider, includeEmptyDirectories, deterministic)
{
}
public PackageBuilder(string path, Func<string, string>? propertyProvider, bool includeEmptyDirectories, bool deterministic, ILogger logger)
: this(path, Path.GetDirectoryName(path)!, propertyProvider, includeEmptyDirectories, deterministic, logger, versionOverride: "")
{
}
public PackageBuilder(string path, Func<string, string>? propertyProvider, bool includeEmptyDirectories, bool deterministic, ILogger logger, string versionOverride)
: this(path, Path.GetDirectoryName(path)!, propertyProvider, includeEmptyDirectories, deterministic, logger, versionOverride)
{
}
public PackageBuilder(string path, string? basePath, Func<string, string>? propertyProvider, bool includeEmptyDirectories)
: this(path, basePath, propertyProvider, includeEmptyDirectories, deterministic: false)
{
}
public PackageBuilder(string path, string? basePath, Func<string, string>? propertyProvider, bool includeEmptyDirectories, bool deterministic, ILogger logger)
: this(path, basePath, propertyProvider, includeEmptyDirectories, deterministic, logger, versionOverride: "")
{
}
public PackageBuilder(string path, string? basePath, Func<string, string>? propertyProvider, bool includeEmptyDirectories, bool deterministic, ILogger logger, string versionOverride)
: this(path, basePath, propertyProvider, includeEmptyDirectories, deterministic, versionOverride)
{
_logger = logger;
}
public PackageBuilder(string path, string? basePath, Func<string, string>? propertyProvider, bool includeEmptyDirectories, bool deterministic)
: this(path, basePath, propertyProvider, includeEmptyDirectories, deterministic, versionOverride: "")
{
}
public PackageBuilder(string path, string? basePath, Func<string, string>? propertyProvider, bool includeEmptyDirectories, bool deterministic, string versionOverride)
: this(includeEmptyDirectories, deterministic)
{
if (!File.Exists(path))
{
throw new PackagingException(
NuGetLogCode.NU5008,
string.Format(CultureInfo.CurrentCulture, Strings.ErrorManifestFileNotFound, path ?? "null"));
}
_versionOverride = versionOverride;
using (Stream stream = File.OpenRead(path))
{
ReadManifest(stream, basePath, propertyProvider);
}
}
public PackageBuilder(Stream stream, string? basePath)
: this(stream, basePath, null)
{
}
public PackageBuilder(Stream stream, string? basePath, Func<string, string>? propertyProvider)
: this(stream, basePath, propertyProvider, "")
{
}
public PackageBuilder(Stream stream, string? basePath, Func<string, string>? propertyProvider, string versionOverride)
: this()
{
_versionOverride = versionOverride;
ReadManifest(stream, basePath, propertyProvider);
}
public PackageBuilder(bool deterministic) :
this(includeEmptyDirectories: false, deterministic: deterministic)
{
}
public PackageBuilder()
: this(includeEmptyDirectories: false, deterministic: false)
{
}
public PackageBuilder(bool deterministic, ILogger logger)
: this(includeEmptyDirectories: false, deterministic: deterministic, logger: logger)
{
}
private PackageBuilder(bool includeEmptyDirectories, bool deterministic)
: this(includeEmptyDirectories: includeEmptyDirectories, deterministic: deterministic, logger: NullLogger.Instance)
{
}
private PackageBuilder(bool includeEmptyDirectories, bool deterministic, ILogger logger)
{
_includeEmptyDirectories = includeEmptyDirectories;
_deterministic = deterministic;
_logger = logger;
Files = new Collection<IPackageFile>();
DependencyGroups = new Collection<PackageDependencyGroup>();
FrameworkReferences = new Collection<FrameworkAssemblyReference>();
FrameworkReferenceGroups = new Collection<FrameworkReferenceGroup>();
ContentFiles = new Collection<ManifestContentFiles>();
PackageAssemblyReferences = new Collection<PackageReferenceSet>();
PackageTypes = new Collection<PackageType>();
Authors = new HashSet<string>();
Owners = new HashSet<string>();
Tags = new HashSet<string>();
TargetFrameworks = new List<NuGetFramework>();
// Just like parameter replacements, these are also case insensitive, for consistency.
Properties = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}
public string DeterministicTimestamp
{
init
{
if (string.IsNullOrEmpty(value) || string.Equals(value, bool.TrueString, StringComparison.OrdinalIgnoreCase))
{
_deterministicTimestamp = DateTimeOffset.UtcNow;
}
else if (string.Equals(value, bool.FalseString, StringComparison.OrdinalIgnoreCase))
{
_deterministic = false;
_deterministicTimestamp = DateTimeOffset.UtcNow;
}
else if (TryParseTimestamp(value, out DateTimeOffset parsedDateTimestamp))
{
_deterministicTimestamp = parsedDateTimestamp;
}
else
{
throw new PackagingException(
NuGetLogCode.NU5502,
string.Format(CultureInfo.CurrentCulture, Strings.ErrorInvalidTimestamp, value));
}
}
}
internal static bool TryParseTimestamp(string timestamp, out DateTimeOffset result)
{
if (long.TryParse(timestamp, NumberStyles.None, CultureInfo.InvariantCulture, out long unixTimeSeconds))
{
result = DateTimeOffset.FromUnixTimeSeconds(unixTimeSeconds);
return true;
}
var parsedDate = ParseRfc3339(timestamp);
if (parsedDate is DateTimeOffset nonNullParsedTimestamp)
{
result = nonNullParsedTimestamp;
return true;
}
result = default;
return false;
}
private static DateTimeOffset? ParseRfc3339(string input)
{
// RFC3339 patterns:
// 1. Full date-time with 'Z' (UTC)
// 2. Full date-time with offset (+HH:mm or -HH:mm)
// 3. Full date-time with fractional seconds and 'Z'
// 4. Full date-time with fractional seconds and offset
string[] formats = {
"yyyy-MM-dd'T'HH:mm:ss'Z'",
"yyyy-MM-dd'T'HH:mm:sszzz",
"yyyy-MM-dd'T'HH:mm:ss.FFFFFFFK", // K handles Z or offset
};
if (DateTimeOffset.TryParseExact(input, formats,
CultureInfo.InvariantCulture,
DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal,
out DateTimeOffset result))
{
return result;
}
return null;
}
// Set to empty to enforce a stricter nullability contract.
// This will be validated in the Save() method before writing the manifest
public string Id { get; set; } = string.Empty;
public NuGetVersion? Version { get; set; }
public RepositoryMetadata? Repository { get; set; }
public LicenseMetadata? LicenseMetadata { get; set; }
public bool HasSnapshotVersion { get; set; }
public string? Title { get; set; }
public ISet<string> Authors { get; private set; }
public ISet<string> Owners { get; private set; }
public Uri? IconUrl { get; set; }
public string? Icon { get; set; }
public Uri? LicenseUrl { get; set; }
public Uri? ProjectUrl { get; set; }
public bool RequireLicenseAcceptance { get; set; }
public bool EmitRequireLicenseAcceptance { get; set; } = true;
public bool Serviceable { get; set; }
public bool DevelopmentDependency { get; set; }
public string? Description { get; set; }
public string? Summary { get; set; }
public string? ReleaseNotes { get; set; }
public string? Language { get; set; }
public string? OutputName { get; set; }
public ISet<string> Tags { get; private set; }
public string? Readme { get; set; }
/// <summary>
/// Exposes the additional properties extracted by the metadata
/// extractor or received from the command line.
/// </summary>
public Dictionary<string, string> Properties { get; private set; }
public string? Copyright { get; set; }
public Collection<PackageDependencyGroup> DependencyGroups { get; private set; }
public ICollection<IPackageFile> Files { get; private set; }
public Collection<FrameworkAssemblyReference> FrameworkReferences { get; private set; }
public Collection<FrameworkReferenceGroup> FrameworkReferenceGroups { get; private set; }
public IList<NuGetFramework> TargetFrameworks { get; set; }
/// <summary>
/// ContentFiles section from the manifest for content v2
/// </summary>
public ICollection<ManifestContentFiles> ContentFiles { get; private set; }
public ICollection<PackageReferenceSet> PackageAssemblyReferences { get; set; }
public ICollection<PackageType> PackageTypes { get; set; }
IEnumerable<string> IPackageMetadata.Authors => Authors;
IEnumerable<string> IPackageMetadata.Owners => Owners;
string IPackageMetadata.Tags => string.Join(" ", Tags);
IEnumerable<PackageReferenceSet> IPackageMetadata.PackageAssemblyReferences => PackageAssemblyReferences;
IEnumerable<PackageDependencyGroup> IPackageMetadata.DependencyGroups => DependencyGroups;
IEnumerable<FrameworkAssemblyReference> IPackageMetadata.FrameworkReferences => FrameworkReferences;
IEnumerable<ManifestContentFiles> IPackageMetadata.ContentFiles => ContentFiles;
IEnumerable<PackageType> IPackageMetadata.PackageTypes => PackageTypes;
IEnumerable<FrameworkReferenceGroup> IPackageMetadata.FrameworkReferenceGroups => FrameworkReferenceGroups;
public Version? MinClientVersion { get; set; }
public void Save(Stream stream)
{
// Make sure we're saving a valid package id
PackageIdValidator.ValidatePackageId(Id!);
// Throw if the package doesn't contain any dependencies nor content
if (!Files.Any() && !DependencyGroups.SelectMany(d => d.Packages).Any() && !FrameworkReferences.Any() && !FrameworkReferenceGroups.Any())
{
throw new PackagingException(NuGetLogCode.NU5017, NuGetResources.CannotCreateEmptyPackage);
}
ValidateDependencies(Version, DependencyGroups);
ValidateFilesUnique(Files);
ValidateReferenceAssemblies(Files, PackageAssemblyReferences);
ValidateFrameworkAssemblies(FrameworkReferences, FrameworkReferenceGroups);
ValidateLicenseFile(Files, LicenseMetadata);
ValidateIconFile(Files, Icon);
ValidateFileFrameworks(Files);
ValidateReadmeFile(Files, Readme);
using (var package = new ZipArchive(stream, ZipArchiveMode.Create, leaveOpen: true))
{
string psmdcpPath = $"package/services/metadata/core-properties/{CalcPsmdcpName()}.psmdcp";
// Validate and write the manifest
WriteManifest(package, DetermineMinimumSchemaVersion(Files, DependencyGroups), psmdcpPath);
// Write the files to the package
SortedSet<string> filesWithoutExtensions = new();
var extensions = WriteFiles(package, filesWithoutExtensions);
extensions.Add("nuspec");
WriteOpcContentTypes(package, extensions, filesWithoutExtensions);
WriteOpcPackageProperties(package, psmdcpPath);
}
}
private static byte[] ReadAllBytes(Stream stream)
{
using (var ms = new MemoryStream())
{
stream.CopyTo(ms);
return ms.ToArray();
}
}
private string CalcPsmdcpName()
{
if (_deterministic)
{
using (var hashFunc = new Sha512HashFunction())
{
foreach (var file in Files)
{
var data = ReadAllBytes(file.GetStream());
hashFunc.Update(data, 0, data.Length);
}
return EncodeHexString(hashFunc.GetHashBytes())!.Substring(0, 32);
}
}
else
{
return Guid.NewGuid().ToString("N", provider: null);
}
}
private static readonly char[] HexValues = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };
// Reference https://github.com/dotnet/corefx/blob/2c2e4a599889652ec579a870054b0f8915ea70fd/src/System.Security.Cryptography.Xml/src/System/Security/Cryptography/Xml/Utils.cs#L736
internal static string? EncodeHexString(byte[] sArray)
{
uint start = 0;
uint end = (uint)sArray.Length;
string? result = null;
if (sArray != null)
{
char[] hexOrder = new char[(end - start) * 2];
uint digit;
for (uint i = start, j = 0; i < end; i++)
{
digit = (uint)((sArray[i] & 0xf0) >> 4);
hexOrder[j++] = HexValues[digit];
digit = (uint)(sArray[i] & 0x0f);
hexOrder[j++] = HexValues[digit];
}
result = new string(hexOrder);
}
return result;
}
private static string CreatorInfo()
{
List<string> creatorInfo = new List<string>();
var assembly = typeof(PackageBuilder).Assembly;
creatorInfo.Add(assembly.FullName!);
#if !IS_CORECLR // CORECLR_TODO: Environment.OSVersion
creatorInfo.Add(Environment.OSVersion.ToString());
#endif
var attribute = assembly.GetCustomAttributes<System.Runtime.Versioning.TargetFrameworkAttribute>().FirstOrDefault();
if (attribute != null)
{
creatorInfo.Add(attribute.FrameworkDisplayName!);
}
return string.Join(";", creatorInfo);
}
private static int DetermineMinimumSchemaVersion(
ICollection<IPackageFile> Files,
ICollection<PackageDependencyGroup> package)
{
if (HasContentFilesV2(Files) || HasIncludeExclude(package) || HasXdtTransformFile(Files))
{
// version 6
return ManifestVersionUtility.XdtTransformationVersion;
}
if (RequiresV4TargetFrameworkSchema(Files))
{
// version 4
return ManifestVersionUtility.TargetFrameworkSupportForDependencyContentsAndToolsVersion;
}
return ManifestVersionUtility.DefaultVersion;
}
private static bool RequiresV4TargetFrameworkSchema(ICollection<IPackageFile> files)
{
// check if any file under Content or Tools has TargetFramework defined
bool hasContentOrTool = files.Any(
f => f.NuGetFramework != null &&
!f.NuGetFramework.IsUnsupported &&
(f.Path.StartsWith(PackagingConstants.Folders.Content + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase) ||
f.Path.StartsWith(PackagingConstants.Folders.Tools + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase)));
if (hasContentOrTool)
{
return true;
}
// now check if the Lib folder has any empty framework folder
bool hasEmptyLibFolder = files.Any(
f => f.NuGetFramework != null &&
f.Path.StartsWith(PackagingConstants.Folders.Lib + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase) &&
f.EffectivePath == PackagingConstants.PackageEmptyFileName);
return hasEmptyLibFolder;
}
private static bool HasContentFilesV2(ICollection<IPackageFile> contentFiles)
{
return contentFiles.Any(file =>
file.Path != null &&
file.Path.StartsWith(PackagingConstants.Folders.ContentFiles + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase));
}
private static bool HasIncludeExclude(IEnumerable<PackageDependencyGroup> dependencyGroups)
{
return dependencyGroups.Any(dependencyGroup =>
dependencyGroup.Packages
.Any(dependency => dependency.Include != null || dependency.Exclude != null));
}
private static bool HasXdtTransformFile(ICollection<IPackageFile> contentFiles)
{
return contentFiles.Any(file =>
file.Path != null &&
file.Path.StartsWith(PackagingConstants.Folders.Content + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase) &&
(file.Path.EndsWith(".install.xdt", StringComparison.OrdinalIgnoreCase) ||
file.Path.EndsWith(".uninstall.xdt", StringComparison.OrdinalIgnoreCase)));
}
private static void ValidateDependencies(SemanticVersion? version,
IEnumerable<PackageDependencyGroup> dependencies)
{
var frameworksMissingPlatformVersion = new HashSet<string>(dependencies
.Select(group => group.TargetFramework)
.Where(groupFramework => groupFramework.HasPlatform && groupFramework.PlatformVersion == FrameworkConstants.EmptyVersion)
.Select(framework => framework.GetShortFolderName()));
if (frameworksMissingPlatformVersion.Any())
{
throw new PackagingException(NuGetLogCode.NU1012, string.Format(CultureInfo.CurrentCulture, Strings.MissingTargetPlatformVersionsFromDependencyGroups, string.Join(", ", frameworksMissingPlatformVersion.OrderBy(str => str))));
}
if (version == null)
{
// We have independent validation for null-versions.
return;
}
foreach (var dep in dependencies.SelectMany(s => s.Packages))
{
PackageIdValidator.ValidatePackageId(dep.Id);
}
}
public static void ValidateReferenceAssemblies(IEnumerable<IPackageFile> files, IEnumerable<PackageReferenceSet> packageAssemblyReferences)
{
var frameworksMissingPlatformVersion = new HashSet<string>(packageAssemblyReferences
.Select(group => group.TargetFramework)
.Where(groupFramework => groupFramework != null && groupFramework.HasPlatform && groupFramework.PlatformVersion == FrameworkConstants.EmptyVersion)
.Select(framework => framework!.GetShortFolderName()));
if (frameworksMissingPlatformVersion.Any())
{
throw new PackagingException(NuGetLogCode.NU1012, string.Format(CultureInfo.CurrentCulture, Strings.MissingTargetPlatformVersionsFromReferenceGroups, string.Join(", ", frameworksMissingPlatformVersion.OrderBy(str => str))));
}
var libFiles = new HashSet<string>(from file in files
where !string.IsNullOrEmpty(file.Path) && file.Path.StartsWith("lib", StringComparison.OrdinalIgnoreCase)
select Path.GetFileName(file.Path), StringComparer.OrdinalIgnoreCase);
foreach (var reference in packageAssemblyReferences.SelectMany(p => p.References))
{
if (!libFiles.Contains(reference) &&
!libFiles.Contains(reference + ".dll") &&
!libFiles.Contains(reference + ".exe") &&
!libFiles.Contains(reference + ".winmd"))
{
throw new PackagingException(NuGetLogCode.NU5018, string.Format(CultureInfo.CurrentCulture, NuGetResources.Manifest_InvalidReference, reference));
}
}
}
private static void ValidateFrameworkAssemblies(IEnumerable<FrameworkAssemblyReference> references, IEnumerable<FrameworkReferenceGroup> referenceGroups)
{
// Check standalone references
var frameworksMissingPlatformVersion = new HashSet<string>(references
.SelectMany(reference => reference.SupportedFrameworks)
.Where(framework => framework.HasPlatform && framework.PlatformVersion == FrameworkConstants.EmptyVersion)
.Select(framework => framework.GetShortFolderName())
);
if (frameworksMissingPlatformVersion.Any())
{
throw new PackagingException(NuGetLogCode.NU1012, string.Format(CultureInfo.CurrentCulture, Strings.MissingTargetPlatformVersionsFromFrameworkAssemblyReferences, string.Join(", ", frameworksMissingPlatformVersion.OrderBy(str => str))));
}
// Check reference groups too
frameworksMissingPlatformVersion = new HashSet<string>(referenceGroups
.Select(group => group.TargetFramework)
.Where(groupFramework => groupFramework.HasPlatform && groupFramework.PlatformVersion == FrameworkConstants.EmptyVersion)
.Select(framework => framework.GetShortFolderName()));
if (frameworksMissingPlatformVersion.Any())
{
throw new PackagingException(NuGetLogCode.NU1012, string.Format(CultureInfo.CurrentCulture, Strings.MissingTargetPlatformVersionsFromFrameworkAssemblyGroups, string.Join(", ", frameworksMissingPlatformVersion.OrderBy(str => str))));
}
}
/// <summary>Looks for the specified file within the package</summary>
/// <param name="filePath">The file path to search for</param>
/// <param name="packageFiles">The list of files to search within</param>
/// <param name="filePathIncorrectCase">If the file was not found, this will be a path which almost matched but had the incorrect case</param>
/// <returns>An <see cref="IPackageFile"/> matching the specified path or <see langword="null" /></returns>
private static IPackageFile? FindFileInPackage(string filePath, IEnumerable<IPackageFile> packageFiles, out string? filePathIncorrectCase)
{
filePathIncorrectCase = null;
var strippedFilePath = PathUtility.StripLeadingDirectorySeparators(filePath);
foreach (var packageFile in packageFiles)
{
var strippedPackageFilePath = PathUtility.StripLeadingDirectorySeparators(packageFile.Path);
// This must use a case-sensitive string comparison, even on systems where file paths are normally case-sensitive.
// This is because Zip files are treated as case-sensitive. (See https://github.com/NuGet/Home/issues/9817)
if (strippedPackageFilePath.Equals(strippedFilePath, StringComparison.Ordinal))
{
// Found the requested file in the package
filePathIncorrectCase = null;
return packageFile;
}
// Check for files that exist with the wrong file casing
else if (filePathIncorrectCase is null && strippedPackageFilePath.Equals(strippedFilePath, StringComparison.OrdinalIgnoreCase))
{
filePathIncorrectCase = strippedPackageFilePath;
}
}
// We searched all of the package files and didn't find what we were looking for
return null;
}
private void ValidateFilesUnique(IEnumerable<IPackageFile> files)
{
var seen = new HashSet<string>(StringComparer.Ordinal);
var duplicates = new HashSet<string>(StringComparer.Ordinal);
foreach (string destination in files.Where(t => t.Path != null).Select(t => PathUtility.GetPathWithDirectorySeparator(t.Path)))
{
if (!seen.Add(destination))
{
duplicates.Add(destination);
}
}
if (duplicates.Any())
{
throw new PackagingException(NuGetLogCode.NU5050, string.Format(CultureInfo.CurrentCulture, NuGetResources.FoundDuplicateFile, string.Join(", ", duplicates)));
}
}
private void ValidateLicenseFile(IEnumerable<IPackageFile> files, LicenseMetadata? licenseMetadata)
{
if (!PackageTypes.Contains(PackageType.SymbolsPackage) && licenseMetadata?.Type == LicenseType.File)
{
var ext = Path.GetExtension(licenseMetadata.License);
if (!string.IsNullOrEmpty(ext) &&
!ext.Equals(".txt", StringComparison.OrdinalIgnoreCase) &&
!ext.Equals(".md", StringComparison.OrdinalIgnoreCase))
{
throw new PackagingException(NuGetLogCode.NU5031, string.Format(CultureInfo.CurrentCulture, NuGetResources.Manifest_LicenseFileExtensionIsInvalid, licenseMetadata.License));
}
if (FindFileInPackage(licenseMetadata.License, files, out var licenseFilePathWithIncorrectCase) is null)
{
string errorMessage;
if (licenseFilePathWithIncorrectCase is null)
{
errorMessage = string.Format(CultureInfo.CurrentCulture, NuGetResources.Manifest_LicenseFileIsNotInNupkg, licenseMetadata.License);
}
else
{
errorMessage = string.Format(CultureInfo.CurrentCulture, NuGetResources.Manifest_LicenseFileIsNotInNupkgWithHint, licenseMetadata.License, licenseFilePathWithIncorrectCase);
}
throw new PackagingException(NuGetLogCode.NU5030, errorMessage);
}
}
}
/// <summary>
/// Given a list of resolved files,
/// determine which file will be used as the icon file and validate its size and extension.
/// </summary>
/// <param name="files">Files resolved from the file entries in the nuspec</param>
/// <param name="iconPath">icon entry found in the .nuspec</param>
/// <exception cref="PackagingException">When a validation rule is not met</exception>
private void ValidateIconFile(IEnumerable<IPackageFile> files, string? iconPath)
{
if (!PackageTypes.Contains(PackageType.SymbolsPackage) && !string.IsNullOrEmpty(iconPath))
{
var ext = Path.GetExtension(iconPath);
if (string.IsNullOrEmpty(ext) || (
!ext.Equals(".jpeg", StringComparison.OrdinalIgnoreCase) &&
!ext.Equals(".jpg", StringComparison.OrdinalIgnoreCase) &&
!ext.Equals(".png", StringComparison.OrdinalIgnoreCase)))
{
throw new PackagingException(
NuGetLogCode.NU5045,
string.Format(CultureInfo.CurrentCulture, NuGetResources.IconInvalidExtension, iconPath));
}
// Validate entry
IPackageFile? iconFile = FindFileInPackage(iconPath!, files, out var iconPathWithIncorrectCase);
if (iconFile is null)
{
string errorMessage;
if (iconPathWithIncorrectCase is null)
{
errorMessage = string.Format(CultureInfo.CurrentCulture, NuGetResources.IconNoFileElement, iconPath);
}
else
{
errorMessage = string.Format(CultureInfo.CurrentCulture, NuGetResources.IconNoFileElementWithHint, iconPath, iconPathWithIncorrectCase);
}
throw new PackagingException(NuGetLogCode.NU5046, errorMessage);
}
try
{
// Validate Icon open file
using (var iconStream = iconFile.GetStream())
{
// Validate file size
long fileSize = iconStream.Length;
if (fileSize > MaxIconFileSize)
{
throw new PackagingException(Common.NuGetLogCode.NU5047, NuGetResources.IconMaxFileSizeExceeded);
}
if (fileSize == 0)
{
throw new PackagingException(Common.NuGetLogCode.NU5047, NuGetResources.IconErrorEmpty);
}
}
}
catch (FileNotFoundException e)
{
throw new PackagingException(
NuGetLogCode.NU5046,
string.Format(CultureInfo.CurrentCulture, NuGetResources.IconCannotOpenFile, iconPath, e.Message));
}
}
}
private static void ValidateFileFrameworks(IEnumerable<IPackageFile> files)
{
var set = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var file in files.Where(t => t.Path != null).Select(t => PathUtility.GetPathWithDirectorySeparator(t.Path)))
{
set.Add(file);
}
var managedCodeConventions = new ManagedCodeConventions(RuntimeGraph.Empty);
var collection = new ContentItemCollection();
collection.Load(set.Select(path => path.Replace('\\', '/')).ToArray());
var patterns = managedCodeConventions.Patterns;
var frameworkPatterns = new List<PatternSet>()
{
patterns.RuntimeAssemblies,
patterns.CompileRefAssemblies,
patterns.CompileLibAssemblies,
patterns.NativeLibraries,
patterns.ResourceAssemblies,
patterns.MSBuildFiles,
patterns.ContentFiles,
patterns.ToolsAssemblies,
patterns.EmbedAssemblies,
patterns.MSBuildTransitiveFiles
};
var itemsWithFrameworkMissingPlatformVersion = new HashSet<string>();
List<ContentItemGroup> targetedItemGroups = new();
foreach (var pattern in frameworkPatterns)
{
targetedItemGroups.Clear();
ContentExtractor.GetContentForPattern(collection, pattern, targetedItemGroups);
foreach (ContentItemGroup group in targetedItemGroups)
{
foreach (ContentItem item in group.Items.NoAllocEnumerate())
{
var framework = (NuGetFramework)item.Properties["tfm"];
if (framework == null)
{
continue;
}
if (framework.HasPlatform && framework.PlatformVersion == FrameworkConstants.EmptyVersion)
{
itemsWithFrameworkMissingPlatformVersion.Add(item.Path);
}
}
}
}
if (itemsWithFrameworkMissingPlatformVersion.Any())
{
throw new PackagingException(NuGetLogCode.NU1012, string.Format(CultureInfo.CurrentCulture, Strings.MissingTargetPlatformVersionsFromIncludedFiles, string.Join(", ", itemsWithFrameworkMissingPlatformVersion.OrderBy(str => str))));
}
}
/// <summary>
/// Validate that the readme file is of the correct size/type and can be opened properly except for Symbol packages.
/// </summary>
/// <param name="files">Files resolved from the file entries in the nuspec</param>
/// <param name="readmePath">readmepath found in the .nuspec</param>
/// <exception cref="PackagingException">When a validation rule is not met</exception>
private void ValidateReadmeFile(IEnumerable<IPackageFile> files, string? readmePath)
{
if (!PackageTypes.Contains(PackageType.SymbolsPackage) && !string.IsNullOrEmpty(readmePath))
{
// Validate readme extension
var extension = Path.GetExtension(readmePath);
if (!string.IsNullOrEmpty(extension) &&
!extension.Equals(NuGetConstants.ReadmeExtension, StringComparison.OrdinalIgnoreCase))
{
throw new PackagingException(
NuGetLogCode.NU5038,
string.Format(CultureInfo.CurrentCulture, NuGetResources.ReadmeFileExtensionIsInvalid, readmePath));
}
// Validate entry
var readmePathStripped = PathUtility.StripLeadingDirectorySeparators(readmePath!);
var readmeFileList = files.Where(f =>
readmePathStripped.Equals(
PathUtility.StripLeadingDirectorySeparators(f.Path),
PathUtility.GetStringComparisonBasedOnOS()));
if (!readmeFileList.Any())
{
throw new PackagingException(
NuGetLogCode.NU5039,
string.Format(CultureInfo.CurrentCulture, NuGetResources.ReadmeNoFileElement, readmePath));
}
IPackageFile readmeFile = readmeFileList.First();
try
{
// Validate Readme open file
using (var readmeStream = readmeFile.GetStream())
{
// Validate file size is not 0
long fileSize = readmeStream.Length;
if (fileSize == 0)
{
throw new PackagingException(
NuGetLogCode.NU5040,
string.Format(CultureInfo.CurrentCulture, NuGetResources.ReadmeErrorEmpty, readmePath));
}
}
}
catch (FileNotFoundException e)
{
throw new PackagingException(
NuGetLogCode.NU5041,
string.Format(CultureInfo.CurrentCulture, NuGetResources.ReadmeCannotOpenFile, readmePath, e.Message));
}
}
}
private void ReadManifest(Stream stream, string? basePath, Func<string, string>? propertyProvider)
{
// Deserialize the document and extract the metadata
Manifest manifest = Manifest.ReadFrom(
stream,
propertyProvider,
validateSchema: true,
overrideVersion: !(string.IsNullOrEmpty(_versionOverride)) ? NuGetVersion.Parse(_versionOverride!) : null);
Populate(manifest.Metadata);
// If there's no base path then ignore the files node
if (basePath != null)
{
if (!manifest.HasFilesNode)
{
AddFiles(basePath, @"**\*", null);
}
else
{
PopulateFiles(basePath, manifest.Files);
}
}
}
public void Populate(ManifestMetadata manifestMetadata)
{
IPackageMetadata metadata = manifestMetadata;
Id = metadata.Id!;
Version = metadata.Version;
Title = metadata.Title;
Authors.AddRange(metadata.Authors);
Owners.AddRange(metadata.Owners);
IconUrl = metadata.IconUrl;
LicenseUrl = metadata.LicenseUrl;
ProjectUrl = metadata.ProjectUrl;
RequireLicenseAcceptance = metadata.RequireLicenseAcceptance;
DevelopmentDependency = metadata.DevelopmentDependency;
Serviceable = metadata.Serviceable;
Description = metadata.Description;
Summary = metadata.Summary;
ReleaseNotes = metadata.ReleaseNotes;
Language = metadata.Language;
Copyright = metadata.Copyright;
MinClientVersion = metadata.MinClientVersion;
Repository = metadata.Repository;
ContentFiles = new Collection<ManifestContentFiles>(manifestMetadata.ContentFiles.ToList());
LicenseMetadata = metadata.LicenseMetadata;
Icon = metadata.Icon;
Readme = metadata.Readme;
if (metadata.Tags != null)
{
Tags.AddRange(ParseTags(metadata.Tags));
}
DependencyGroups.AddRange(metadata.DependencyGroups);
FrameworkReferences.AddRange(metadata.FrameworkReferences);
FrameworkReferenceGroups.AddRange(metadata.FrameworkReferenceGroups);
if (manifestMetadata.PackageAssemblyReferences != null)
{
PackageAssemblyReferences.AddRange(manifestMetadata.PackageAssemblyReferences);
}
if (manifestMetadata.PackageTypes != null)
{
PackageTypes = new Collection<PackageType>(metadata.PackageTypes.ToList());
}
}
public void PopulateFiles(string basePath, IEnumerable<ManifestFile> files)
{
foreach (var file in files)
{
AddFiles(basePath, file.Source!, file.Target, file.Exclude);
}
}
private ZipArchiveEntry CreateEntry(ZipArchive package, string entryName, CompressionLevel compressionLevel)
{
var entry = package.CreateEntry(entryName, compressionLevel);
if (_deterministic)
{
entry.LastWriteTime = _deterministicTimestamp;
}
return entry;
}
private static ZipArchiveEntry CreatePackageFileEntry(ZipArchive package, string entryName, DateTimeOffset timeOffset, CompressionLevel compressionLevel, StringBuilder warningMessage)
{
var entry = package.CreateEntry(entryName, compressionLevel);
if (timeOffset.UtcDateTime < ZipFormatMinDate)
{
warningMessage.AppendLine(StringFormatter.ZipFileTimeStampModifiedMessage(entryName, timeOffset.DateTime.ToShortDateString(), ZipFormatMinDate.ToShortDateString()));
entry.LastWriteTime = ZipFormatMinDate;
}
else if (timeOffset.UtcDateTime > ZipFormatMaxDate)
{
warningMessage.AppendLine(StringFormatter.ZipFileTimeStampModifiedMessage(entryName, timeOffset.DateTime.ToShortDateString(), ZipFormatMaxDate.ToShortDateString()));
entry.LastWriteTime = ZipFormatMaxDate;
}
else
{
entry.LastWriteTime = timeOffset.UtcDateTime;
}
return entry;
}
private void WriteManifest(ZipArchive package, int minimumManifestVersion, string psmdcpPath)
{
var path = Id + PackagingConstants.ManifestExtension;
WriteOpcManifestRelationship(package, path, psmdcpPath);
var entry = CreateEntry(package, path, CompressionLevel.Optimal);
using (var stream = entry.Open())
{
var manifest = Manifest.Create(this);
manifest.Save(stream, minimumManifestVersion);
}
}
private SortedSet<string> WriteFiles(ZipArchive package, SortedSet<string> filesWithoutExtensions)
{
var extensions = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
var warningMessage = new StringBuilder();
// Add files that might not come from expanding files on disk
foreach (IPackageFile file in new SortedSet<IPackageFile>(Files, new NormalizedPathComparer()))
{
using (Stream stream = file.GetStream())
{
CreatePart(
package,
file.Path,
stream,
lastWriteTime: _deterministic ? _deterministicTimestamp : file.LastWriteTime,
warningMessage);
var fileExtension = Path.GetExtension(file.Path);
// We have files without extension (e.g. the executables for Nix)
if (!string.IsNullOrEmpty(fileExtension))
{
extensions.Add(fileExtension.Substring(1));
}
else
{
#if NETCOREAPP
filesWithoutExtensions.Add($"/{file.Path.Replace("\\", "/", StringComparison.Ordinal)}");
#else
filesWithoutExtensions.Add($"/{file.Path.Replace("\\", "/")}");
#endif
}
}
}
if (warningMessage.Length > Environment.NewLine.Length)
{
warningMessage.Length -= Environment.NewLine.Length;
}
var warningMessageString = warningMessage.ToString();
if (!string.IsNullOrEmpty(warningMessageString))
{
_logger?.Log(PackagingLogMessage.CreateWarning(StringFormatter.ZipFileTimeStampModifiedWarning(warningMessageString), NuGetLogCode.NU5132));
}
return extensions;
}
public void AddFiles(string basePath, string source, string? destination, string? exclude = null)
{
exclude = exclude?.Replace('\\', Path.DirectorySeparatorChar);
List<PhysicalPackageFile> searchFiles = ResolveSearchPattern(basePath, source.Replace('\\', Path.DirectorySeparatorChar), destination, _includeEmptyDirectories).ToList();
if (_includeEmptyDirectories)
{
// we only allow empty directories which are under known root folders.
searchFiles.RemoveAll(file => Path.GetFileName(file.TargetPath) == PackagingConstants.PackageEmptyFileName
&& !IsKnownFolder(file.TargetPath));
}
ExcludeFiles(searchFiles, basePath, exclude);
// Don't throw if the exclude is what made this find no files. Adding files from
// project.json ends up calling this one file at a time where some may be filtered out.
if (!PathResolver.IsWildcardSearch(source) && !PathResolver.IsDirectoryPath(source) && !searchFiles.Any() && string.IsNullOrEmpty(exclude))
{
throw new PackagingException(NuGetLogCode.NU5019,
string.Format(CultureInfo.CurrentCulture, NuGetResources.PackageAuthoring_FileNotFound, source));
}
Files.AddRange(searchFiles);
}
internal static IEnumerable<PhysicalPackageFile> ResolveSearchPattern(string basePath, string searchPath, string? targetPath, bool includeEmptyDirectories)
{
string normalizedBasePath;
IEnumerable<PathResolver.SearchPathResult> searchResults = PathResolver.PerformWildcardSearch(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
}
);
}
/// <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 = PathResolver.IsDirectoryPath(searchPattern);
bool isWildcardSearch = PathResolver.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);
}
/// <summary>
/// Returns true if the path uses a known folder root.
/// </summary>
private static bool IsKnownFolder(string targetPath)
{
if (targetPath != null)
{
var parts = targetPath.Split(
new char[] { '\\', '/' },
StringSplitOptions.RemoveEmptyEntries);
// exclude things in the root of the directory, this is not allowed
// for any of the v3 folders.
// example: an empty 'native' folder does not have a TxM and cannot be used.
if (parts.Length > 1)
{
var topLevelDirectory = parts.FirstOrDefault();
return PackagingConstants.Folders.Known.Any(folder =>
folder.Equals(topLevelDirectory, StringComparison.OrdinalIgnoreCase));
}
}
return false;
}
private static void ExcludeFiles(List<PhysicalPackageFile> 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 void CreatePart(ZipArchive package, string path, Stream sourceStream, DateTimeOffset lastWriteTime, StringBuilder warningMessage)
{
if (PackageHelper.IsNuspec(path))
{
return;
}
string entryName = CreatePartEntryName(path);
var entry = CreatePackageFileEntry(package, entryName, lastWriteTime, CompressionLevel.Optimal, warningMessage);
using (var stream = entry.Open())
{
sourceStream.CopyTo(stream);
}
}
internal static string CreatePartEntryName(string path)
{
// Only the segments between the path separators should be escaped
var segments = path.Split(new[] { '/', '\\', Path.DirectorySeparatorChar }, StringSplitOptions.None)
.Select(Uri.EscapeDataString);
var escapedPath = string.Join("/", segments);
// retrieve only relative path with resolved . or ..
return GetStringForPartUri(escapedPath);
}
internal static string GetStringForPartUri(string escapedPath)
{
//Create an absolute URI to get the refinement on the relative path
var partUri = new Uri(DefaultUri, escapedPath);
// Get the safe-unescaped form of the URI first. This will unescape all the characters
Uri safeUnescapedUri = new Uri(partUri.GetComponents(UriComponents.Path, UriFormat.SafeUnescaped), UriKind.Relative);
return safeUnescapedUri.GetComponents(UriComponents.SerializationInfoString, UriFormat.Unescaped);
}
/// <summary>
/// Tags come in this format. tag1 tag2 tag3 etc..
/// </summary>
private static IEnumerable<string> ParseTags(string? tags)
{
Debug.Assert(tags != null);
return from tag in tags!.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries)
select tag.Trim();
}
private void WriteOpcManifestRelationship(ZipArchive package, string path, string psmdcpPath)
{
ZipArchiveEntry relsEntry = CreateEntry(package, "_rels/.rels", CompressionLevel.Optimal);
XNamespace relationships = "http://schemas.openxmlformats.org/package/2006/relationships";
XDocument document = new XDocument(
new XElement(relationships + "Relationships",
new XElement(relationships + "Relationship",
new XAttribute("Type", "http://schemas.microsoft.com/packaging/2010/07/manifest"),
new XAttribute("Target", $"/{path}"),
new XAttribute("Id", GenerateRelationshipId($"/{path}"))),
new XElement(relationships + "Relationship",
new XAttribute("Type", "http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties"),
new XAttribute("Target", $"/{psmdcpPath}"),
new XAttribute("Id", GenerateRelationshipId($"/{psmdcpPath}")))
)
);
using (var writer = new StreamWriter(relsEntry.Open()))
{
document.Save(writer);
writer.Flush();
}
}
private void WriteOpcContentTypes(ZipArchive package, SortedSet<string> extensions, SortedSet<string> filesWithoutExtensions)
{
// OPC backwards compatibility
ZipArchiveEntry relsEntry = CreateEntry(package, "[Content_Types].xml", CompressionLevel.Optimal);
XNamespace content = "http://schemas.openxmlformats.org/package/2006/content-types";
XElement element = new XElement(content + "Types",
new XElement(content + "Default",
new XAttribute("Extension", "rels"),
new XAttribute("ContentType", "application/vnd.openxmlformats-package.relationships+xml")),
new XElement(content + "Default",
new XAttribute("Extension", "psmdcp"),
new XAttribute("ContentType", "application/vnd.openxmlformats-package.core-properties+xml"))
);
foreach (var extension in extensions)
{
element.Add(
new XElement(content + "Default",
new XAttribute("Extension", extension),
new XAttribute("ContentType", "application/octet")
)
);
}
foreach (var file in filesWithoutExtensions)
{
element.Add(
new XElement(content + "Override",
new XAttribute("PartName", file),
new XAttribute("ContentType", "application/octet")
)
);
}
XDocument document = new XDocument(element);
using (var writer = new StreamWriter(relsEntry.Open()))
{
document.Save(writer);
writer.Flush();
}
}
// OPC backwards compatibility for package properties
private void WriteOpcPackageProperties(ZipArchive package, string psmdcpPath)
{
ZipArchiveEntry packageEntry = CreateEntry(package, psmdcpPath, CompressionLevel.Optimal);
var dcText = "http://purl.org/dc/elements/1.1/";
XNamespace dc = dcText;
var dctermsText = "http://purl.org/dc/terms/";
var xsiText = "http://www.w3.org/2001/XMLSchema-instance";
XNamespace core = "http://schemas.openxmlformats.org/package/2006/metadata/core-properties";
XDocument document = new XDocument(
new XElement(core + "coreProperties",
new XAttribute(XNamespace.Xmlns + "dc", dcText),
new XAttribute(XNamespace.Xmlns + "dcterms", dctermsText),
new XAttribute(XNamespace.Xmlns + "xsi", xsiText),
new XElement(dc + "creator", string.Join(", ", Authors)),
new XElement(dc + "description", Description),
new XElement(dc + "identifier", Id),
new XElement(core + "version", Version!.ToString()),
//new XElement(core + "language", Language),
new XElement(core + "keywords", ((IPackageMetadata)this).Tags),
//new XElement(dc + "title", Title),
new XElement(core + "lastModifiedBy", CreatorInfo())
)
);
using (var writer = new StreamWriter(packageEntry.Open()))
{
document.Save(writer);
writer.Flush();
}
}
// Generate a relationship id for compatibility
private string GenerateRelationshipId(string path)
{
using (var hashFunc = new Sha512HashFunction())
{
var data = System.Text.Encoding.UTF8.GetBytes(path);
hashFunc.Update(data, 0, data.Length);
var hash = hashFunc.GetHashBytes();
var hex = EncodeHexString(hash);
return "R" + hex!.Substring(0, 16);
}
}
private class NormalizedPathComparer : IComparer<IPackageFile>
{
public int Compare(IPackageFile? x, IPackageFile? y)
{
string xPathNormalized = x!.Path.Replace('\\', '/');
string yPathNormalized = y!.Path.Replace('\\', '/');
return String.CompareOrdinal(xPathNormalized, yPathNormalized);
}
}
}
}
|