File: SdkKnownFrameworkReferenceResolver.cs
Web Access
Project: Microsoft.NET.ProjectData.csproj (Microsoft.NET.ProjectData)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Collections.Concurrent;
using System.Xml.Linq;

namespace Microsoft.NET.ProjectData;

/// <summary>
/// Reads the selected .NET SDK's bundled-version tables so cache files can
/// represent SDK-known NuGet packs by logical package name.
/// </summary>
internal static class SdkKnownPackResolver
{
	private static readonly ConcurrentDictionary<string, Lazy<SdkKnownPackSet>> Cache = new(StringComparers.Paths);
	private static readonly KnownPackItemMetadata[] KnownAnalyzerPackItemMetadata =
	[
		new("KnownILLinkPack", "ILLinkPackVersion"),
	];

	public static bool TryGetTargetingPackVersionForSdk(
		string? sdkPath,
		string packName,
		string? targetFramework,
		out string? targetingPackVersion)
	{
		targetingPackVersion = null;
		if (string.IsNullOrWhiteSpace(sdkPath) || string.IsNullOrWhiteSpace(packName) || string.IsNullOrWhiteSpace(targetFramework))
			return false;

		return GetReferenceSet(sdkPath!).TryGetTargetingPackVersion(packName, targetFramework, out targetingPackVersion);
	}

	public static bool TryGetSdkAnalyzerPackVersionForSdk(
		string? sdkPath,
		string packageId,
		string? targetFramework,
		out string? packageVersion)
	{
		packageVersion = null;
		if (string.IsNullOrWhiteSpace(sdkPath)
			|| string.IsNullOrWhiteSpace(packageId)
			|| string.IsNullOrWhiteSpace(targetFramework))
		{
			return false;
		}

		return GetReferenceSet(sdkPath!).TryGetSdkAnalyzerPackVersion(packageId, targetFramework, out packageVersion);
	}

	private static SdkKnownPackSet GetReferenceSet(string sdkPath)
	{
		string normalizedSdkPath = NormalizeSdkPath(sdkPath);
		// Wrap the IO+XML parse in Lazy so racing callers can't trigger duplicate parses.
		return Cache.GetOrAdd(
			normalizedSdkPath,
			static path => new Lazy<SdkKnownPackSet>(() => ParseReferences(path), LazyThreadSafetyMode.ExecutionAndPublication)).Value;
	}

	private static SdkKnownPackSet ParseReferences(string sdkPath)
	{
		string propsPath = Path.Combine(sdkPath, "Microsoft.NETCoreSdk.BundledVersions.props");
		if (!File.Exists(propsPath))
		{
			return SdkKnownPackSet.Empty;
		}

		XDocument doc;
		try
		{
			doc = XDocument.Load(propsPath, LoadOptions.None);
		}
		catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or System.Xml.XmlException)
		{
			System.Diagnostics.Trace.TraceWarning(
				"[lscache] Failed to parse known-pack metadata from {0}: {1}",
				propsPath,
				ex.Message);
			return SdkKnownPackSet.Empty;
		}

		List<KnownFrameworkReference> references = [];
		List<KnownSdkAnalyzerPack> analyzerPacks = [];
		foreach (XElement element in doc.Descendants())
		{
			if (element.Name.LocalName == "KnownFrameworkReference")
			{
				string? targetFramework = (string?)element.Attribute("TargetFramework");
				string? targetingPackName = (string?)element.Attribute("TargetingPackName");
				string? targetingPackVersion = (string?)element.Attribute("TargetingPackVersion");
				if (string.IsNullOrWhiteSpace(targetFramework)
					|| string.IsNullOrWhiteSpace(targetingPackName)
					|| string.IsNullOrWhiteSpace(targetingPackVersion))
				{
					continue;
				}

				references.Add(new KnownFrameworkReference(targetFramework!, targetingPackName!, targetingPackVersion!));
				continue;
			}

			if (TryGetKnownAnalyzerPackVersionMetadata(element.Name.LocalName, out string? versionMetadataName))
			{
				string? targetFramework = (string?)element.Attribute("TargetFramework");
				string? packageId = (string?)element.Attribute("Include");
				string? packageVersion = (string?)element.Attribute(versionMetadataName);
				if (string.IsNullOrWhiteSpace(targetFramework)
					|| string.IsNullOrWhiteSpace(packageId)
					|| string.IsNullOrWhiteSpace(packageVersion))
				{
					continue;
				}

				analyzerPacks.Add(new KnownSdkAnalyzerPack(targetFramework!, packageId!, packageVersion!));
			}
		}

		return new SdkKnownPackSet(references.ToArray(), analyzerPacks.ToArray());
	}

	private static string NormalizeSdkPath(string sdkPath)
	{
		string full = Path.GetFullPath(sdkPath);
		if (string.Equals(Path.GetFileName(full.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)), "Sdks", StringComparison.OrdinalIgnoreCase))
		{
			full = Path.GetDirectoryName(full.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)) ?? full;
		}

		return full;
	}

	private static bool TryGetKnownAnalyzerPackVersionMetadata(string itemType, out string versionMetadataName)
	{
		foreach (KnownPackItemMetadata metadata in KnownAnalyzerPackItemMetadata)
		{
			if (string.Equals(metadata.ItemType, itemType, StringComparison.OrdinalIgnoreCase))
			{
				versionMetadataName = metadata.VersionMetadataName;
				return true;
			}
		}

		versionMetadataName = string.Empty;
		return false;
	}

	private sealed class SdkKnownPackSet
	{
		public static readonly SdkKnownPackSet Empty = new([], []);

		private readonly KnownFrameworkReference[] references;
		private readonly KnownSdkAnalyzerPack[] analyzerPacks;

		public SdkKnownPackSet(KnownFrameworkReference[] references, KnownSdkAnalyzerPack[] analyzerPacks)
		{
			this.references = references;
			this.analyzerPacks = analyzerPacks;
		}

		public bool TryGetTargetingPackVersion(string packName, string targetFramework, out string? targetingPackVersion)
		{
			foreach (KnownFrameworkReference reference in this.references)
			{
				if (TargetFrameworkMatches(reference.TargetFramework, targetFramework)
					&& string.Equals(reference.TargetingPackName, packName, StringComparison.OrdinalIgnoreCase))
				{
					targetingPackVersion = reference.TargetingPackVersion;
					return true;
				}
			}

			targetingPackVersion = null;
			return false;
		}

		public bool TryGetSdkAnalyzerPackVersion(string packageId, string targetFramework, out string? packageVersion)
		{
			foreach (KnownSdkAnalyzerPack pack in this.analyzerPacks)
			{
				if (TargetFrameworkMatches(pack.TargetFramework, targetFramework)
					&& string.Equals(pack.PackageId, packageId, StringComparison.OrdinalIgnoreCase))
				{
					packageVersion = pack.PackageVersion;
					return true;
				}
			}

			packageVersion = null;
			return false;
		}

		private static bool TargetFrameworkMatches(string knownTargetFramework, string requestedTargetFramework)
		{
			if (string.Equals(knownTargetFramework, requestedTargetFramework, StringComparison.OrdinalIgnoreCase))
			{
				return true;
			}

			// Match on identifier + (major, minor) rather than identifier + major alone.
			// Earlier the fallback compared only the first digit run, so ``net4.8`` matched
			// ``net4.0`` and a request for ``net8.1`` could resolve against ``net8.0`` data.
			if (!TryParseTfm(knownTargetFramework, out string knownId, out Version knownVersion)
				|| !TryParseTfm(requestedTargetFramework, out string requestedId, out Version requestedVersion))
			{
				return false;
			}

			return string.Equals(knownId, requestedId, StringComparison.OrdinalIgnoreCase)
				&& knownVersion.Major == requestedVersion.Major
				&& knownVersion.Minor == requestedVersion.Minor;
		}

		private static bool TryParseTfm(string? tfm, out string identifier, out Version version)
		{
			identifier = string.Empty;
			version = new Version(0, 0);
			if (string.IsNullOrEmpty(tfm))
			{
				return false;
			}

			int versionStart = -1;
			for (int i = 0; i < tfm.Length; i++)
			{
				if (char.IsDigit(tfm[i]))
				{
					versionStart = i;
					break;
				}
			}
			if (versionStart <= 0)
			{
				return false;
			}

			identifier = tfm[..versionStart];
			string versionPart = tfm[versionStart..];
			// ``Version.TryParse`` requires at least ``major.minor``; pad bare ``net8`` → ``8.0``.
			if (!versionPart.Contains('.'))
			{
				versionPart += ".0";
			}
			if (Version.TryParse(versionPart, out Version? parsed))
			{
				version = parsed;
				return true;
			}
			return false;
		}
	}

	private readonly struct KnownFrameworkReference
	{
		public KnownFrameworkReference(string targetFramework, string targetingPackName, string targetingPackVersion)
		{
			this.TargetFramework = targetFramework;
			this.TargetingPackName = targetingPackName;
			this.TargetingPackVersion = targetingPackVersion;
		}

		public string TargetFramework { get; }
		public string TargetingPackName { get; }
		public string TargetingPackVersion { get; }
	}

	private readonly struct KnownSdkAnalyzerPack
	{
		public KnownSdkAnalyzerPack(string targetFramework, string packageId, string packageVersion)
		{
			this.TargetFramework = targetFramework;
			this.PackageId = packageId;
			this.PackageVersion = packageVersion;
		}

		public string TargetFramework { get; }
		public string PackageId { get; }
		public string PackageVersion { get; }
	}

	private readonly struct KnownPackItemMetadata
	{
		public KnownPackItemMetadata(string itemType, string versionMetadataName)
		{
			this.ItemType = itemType;
			this.VersionMetadataName = versionMetadataName;
		}

		public string ItemType { get; }
		public string VersionMetadataName { get; }
	}
}