Rasagar/Library/PackageCache/com.unity.package-validation-suite/Lib/PvpXray/ManifestBaselinesVerifier.cs
2024-08-26 23:07:20 +03:00

213 lines
8.7 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
namespace PvpXray
{
class ManifestBaselinesVerifier : Verifier.IChecker
{
public static string[] Checks => new[]
{
"PVP-160-1", // Direct dependencies must have been promoted, if not built-in (and listed the editor manifest for the package's declared Unity min-version), or in the verification set.
"PVP-161-1", // The editor min-version of recursive dependencies may not be higher than package's own min-version (ignoring built-in or missing packages).
"PVP-162-1", // The recursive dependency chain of the package may not contain cycles (ignoring built-in or missing packages).
};
public static int PassCount => 0;
internal struct UnityVersionRequirement
{
public readonly string Major;
public readonly string Minor;
public bool IsAny => Major == null;
public string RequiresSuchAndSuch => IsAny ? "does not specify a minimum Unity version" : $"requires Unity {Expand(null)}";
public UnityVersionRequirement(Json manifest)
{
var major = manifest["unity"];
var minor = manifest["unityRelease"];
if (major.IsPresent)
{
Major = major.String;
Minor = minor.IfPresent?.String;
}
else
{
Major = Minor = null;
}
}
public string Expand(string appendIfNoMinor) => Major == null ? null : Minor == null ? Major + appendIfNoMinor : $"{Major}.{Minor}";
public bool Equals(UnityVersionRequirement other) => Major == other.Major && Minor == other.Minor;
public override bool Equals(object obj) => obj is UnityVersionRequirement other && Equals(other);
public override int GetHashCode() => unchecked(((Major?.GetHashCode() ?? 0) * 397) ^ (Minor?.GetHashCode() ?? 0));
public static bool operator ==(UnityVersionRequirement left, UnityVersionRequirement right) => left.Equals(right);
public static bool operator !=(UnityVersionRequirement left, UnityVersionRequirement right) => !left.Equals(right);
public bool IsHigherThan(UnityVersionRequirement other, PackageId thisPackage, PackageId otherPackage, out string error, bool useLegacyMessageFormat = false)
{
error = null;
if (this.IsAny) return false;
if (other.IsAny)
{
error = $"{thisPackage} {RequiresSuchAndSuch}, but {otherPackage} {other.RequiresSuchAndSuch}";
return true;
}
if (NaturalCompare(this.Expand(".0f1"), other.Expand(".0f1")) > 0)
{
error = useLegacyMessageFormat
? $"{thisPackage} {RequiresSuchAndSuch}, but {otherPackage} only requires {other.Expand(null)}"
: $"{thisPackage} {RequiresSuchAndSuch}, but {otherPackage} {other.RequiresSuchAndSuch}";
return true;
}
return false;
}
}
// .NET 7 built-in
static bool IsAsciiDigit(char c) => c >= '0' && c <= '9';
static void NaturalCompareReadComponent(string str, int startIndex, out int endIndex, out bool isNumber)
{
isNumber = startIndex < str.Length && IsAsciiDigit(str[startIndex]);
for (endIndex = startIndex; endIndex < str.Length && IsAsciiDigit(str[endIndex]) == isNumber; ++endIndex)
{
}
}
internal static int NaturalCompare(string a, string b)
{
var ai = 0;
var bi = 0;
while (ai < a.Length && bi < b.Length)
{
NaturalCompareReadComponent(a, ai, out var aj, out var aIsNumber);
NaturalCompareReadComponent(b, bi, out var bj, out var bIsNumber);
var aLen = aj - ai;
var bLen = bj - bi;
if (aIsNumber && bIsNumber)
{
// Skip leading '0'.
while (aLen > 0 && a[ai] == '0') { --aLen; ++ai; }
while (bLen > 0 && b[bi] == '0') { --bLen; ++bi; }
// If the numbers have different length, that determines the order.
// Otherwise, we can simply fall back to a string compare.
if (aLen != bLen) return aLen - bLen;
}
var result = string.CompareOrdinal(a, ai, b, bi, Math.Min(aLen, bLen));
if (result != 0) return result;
if (aLen != bLen) return aLen - bLen;
ai = aj;
bi = bj;
}
var aRemaining = a.Length - ai;
var bRemaining = b.Length - bi;
return aRemaining - bRemaining;
}
public ManifestBaselinesVerifier(Verifier.Context context)
{
_ = context.HttpClient; // Bail early if running offline.
var alreadyProcessed = new HashSet<PackageId>();
var packageUnderTest = new PackageId(context.Manifest);
var packageUnderTestMinUnity = new UnityVersionRequirement(context.Manifest);
var path = new List<PackageId>();
WalkDependencies(packageUnderTest);
return;
void CheckThatDirectDependencyIsBuiltIn(PackageId package)
{
if (!packageUnderTestMinUnity.IsAny)
{
var editorVersion = packageUnderTestMinUnity.Expand(".0f1");
if (context.TryFetchEditorManifestBaseline(editorVersion, out var editorManifestPackages))
{
// Package found in editor manifest.
if (editorManifestPackages.Contains(package)) return;
}
else
{
context.AddError("PVP-160-1", $"Editor manifest for Unity {editorVersion} is unavailable; built-in packages cannot be determined");
}
}
context.AddError("PVP-160-1", package.ToString());
}
void WalkDependencies(PackageId package)
{
path.Add(package);
try
{
for (var i = 0; i < path.Count - 1; ++i)
{
if (path[i].Name == package.Name)
{
context.AddError("PVP-162-1", string.Join(" -> ", path.Skip(i)));
return;
}
}
if (!alreadyProcessed.Add(package)) return;
Json manifest;
if (package == packageUnderTest) manifest = context.Manifest;
else if (context.TryFetchPackageBaseline(package, out var baseline)) manifest = baseline.Manifest;
else
{
if (path.Count == 2) CheckThatDirectDependencyIsBuiltIn(package);
return;
}
var minUnity = new UnityVersionRequirement(manifest);
if (minUnity.IsHigherThan(packageUnderTestMinUnity, package, packageUnderTest, out var error, useLegacyMessageFormat: true))
{
context.AddError("PVP-161-1", error);
}
foreach (var m in manifest["dependencies"].MembersIfPresent)
{
PackageId dependency;
try
{
dependency = new PackageId(m.Key, m.String);
}
catch (ArgumentException)
{
// Ignore invalid dependencies.
continue;
}
WalkDependencies(dependency);
}
}
catch (SimpleJsonException e)
{
var message = $"{package}: {e.Message}";
if (path.Count == 1) context.AddError("PVP-160-1", message);
context.AddError("PVP-161-1", message);
context.AddError("PVP-162-1", message);
}
finally
{
path.RemoveAt(path.Count - 1);
}
}
}
public void CheckItem(Verifier.PackageFile file, int passIndex) => throw new NotImplementedException();
public void Finish() { }
}
}