using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; namespace PvpXray { static class ManifestVerifierExtensions { public static Json Unless(this Json elm, bool condition) => condition ? null : elm; public static IEnumerable Unless(this IEnumerable elm, bool condition) => condition ? null : elm; } // These checks are special (because they're actually checked in the // Context), and must be handled in a separate checker in case the // main ManifestVerifier checker throws FailAll. class ManifestContextVerifier : Verifier.IChecker { public static string[] Checks => new[] { "PVP-100-1", "PVP-100-2" }; public static int PassCount => 0; public ManifestContextVerifier(Verifier.Context context) { foreach (var (checkId, error) in context.ManifestContextErrors) context.AddError(checkId, error); } public void CheckItem(Verifier.PackageFile file, int passIndex) { } public void Finish() { } } class ManifestVerifier : Verifier.IChecker { const RegexOptions k_IgnoreCase = RegexOptions.IgnoreCase | RegexOptions.CultureInvariant; // IgnoreCase MUST be used with CultureInvariant. public struct Requirement { public string Message; public Func Func; // Allow implicit conversion from Regex, which (unlike a helper method) gets us proper syntax highlighting. public static implicit operator Requirement(Regex regex) => new Requirement { Message = regex.Options.HasFlag(RegexOptions.IgnoreCase) ? $"must match {regex} (case insensitive)" : $"must match {regex}", Func = json => regex.IsMatch(json.String), }; } static Requirement Fail(string message) => new Requirement { Message = message, Func = _ => false }; static bool IsUnityPackage(Json manifest) => manifest["name"].String.StartsWithOrdinal("com.unity."); static readonly Requirement k_NonEmpty = new Requirement { Message = "must be a non-empty string", Func = json => json.String != "" }; static readonly Requirement k_ValidCompany = new Regex(@"^(com\.unity\.|com\.autodesk\.|com\.havok\.|com\.ptc\.)"); static Requirement Literal(string literal) => new Requirement { Message = $"must be the literal string \"{literal}\"", Func = json => json.String == literal }; // REMEMBER: Checks must not be changed once added. Any modifications must be implemented as a NEW check. // These checks first selects one or more locations in the manifest, then applies a requirement to these locations. // This split allows for better and more consistent error reporting. static readonly (string, Func, Requirement)[] k_LocationChecks = { ("PVP-101-1", m => m["name"], new Regex(@"$(? m["name"], PackageId.ValidName), ("PVP-101-1", m => m["version"], PackageId.ValidSemVer), ("PVP-104-1", m => m["displayName"], new Regex(@"^(?!unity).*$", k_IgnoreCase)), ("PVP-102-1", m => m["displayName"], new Regex("^[ a-zA-Z0-9]{1,50}$")), ("PVP-102-1", m => m["unity"].IfPresent, new Regex(@"^[0-9]{4}\.[1-9][0-9]*$")), ("PVP-102-1", m => m["unityRelease"].IfPresent, new Regex(@"^[0-9]+[abf][1-9][0-9]*$")), ("PVP-102-1", m => m["unityRelease"].IfPresent.Unless(m["unity"].IsPresent), Fail("requires that the 'unity' key is present")), ("PVP-102-1", m => m["dependencies"].MembersIfPresent.Where(e => !PackageId.ValidName.IsMatch(e.Key)), Fail($"key must match {PackageId.ValidName}")), ("PVP-102-1", m => m["dependencies"].MembersIfPresent, PackageId.ValidSemVer), ("PVP-102-2", m => m["displayName"], new Regex("^[ a-zA-Z0-9]{1,50}$")), ("PVP-102-2", m => m["unity"].IfPresent, new Regex(@"^[0-9]{4}\.[1-9]$")), ("PVP-102-2", m => m["unityRelease"].IfPresent, new Regex(@"^[0-9]+[abf][1-9][0-9]*$")), ("PVP-102-2", m => m["unityRelease"].IfPresent.Unless(m["unity"].IsPresent), Fail("requires that the 'unity' key is present")), ("PVP-102-2", m => m["dependencies"].MembersIfPresent.Where(e => !PackageId.ValidName.IsMatch(e.Key)), Fail($"key must match {PackageId.ValidName}")), ("PVP-102-2", m => m["dependencies"].MembersIfPresent.Unless(m["type"].IsPresent && m["type"].String == "feature"), PackageId.ValidSemVer), ("PVP-102-3", m => m["displayName"], new Regex("^[ a-zA-Z0-9]{1,50}$")), ("PVP-102-3", m => m["unity"].IfPresent, new Regex(@"^[0-9]{4}\.[0-9]$")), ("PVP-102-3", m => m["unityRelease"].IfPresent, new Regex(@"^[0-9]+[abf][1-9][0-9]*$")), ("PVP-102-3", m => m["unityRelease"].IfPresent.Unless(m["unity"].IsPresent), Fail("requires that the 'unity' key is present")), ("PVP-102-3", m => m["dependencies"].MembersIfPresent.Where(e => !PackageId.ValidName.IsMatch(e.Key)), Fail($"key must match {PackageId.ValidName}")), ("PVP-102-3", m => m["dependencies"].MembersIfPresent.Unless(m["type"].IsPresent && m["type"].String == "feature"), PackageId.ValidSemVer), ("PVP-108-1", m => m["description"], new Regex("(?s)^.{50,}")), ("PVP-103-1", m => m.Unless(!IsUnityPackage(m))?["author"].IfPresent, Fail("must not be specified in Unity packages")), ("PVP-103-1", m => m.Unless(IsUnityPackage(m))?["author"]["name"], k_NonEmpty), ("PVP-105-1", m => m["name"], k_ValidCompany), ("PVP-110-1", m => m["dist"].IfPresent, Fail("key must not be present")), ("PVP-111-1", m => m["repository"]["url"], k_NonEmpty), ("PVP-111-1", m => m["repository"]["revision"], XrayUtils.Sha1Regex), ("PVP-112-1", m => m["dependencies"].MembersIfPresent.Unless(m["type"].IfPresent?.String != "feature"), Literal("default")), ("PVP-113-1", m => m["type"].IfPresent, new Regex(@"^(feature|template)$")), }; public static string[] Checks => k_LocationChecks.Select(v => v.Item1).Distinct().ToArray(); public static int PassCount => 0; public ManifestVerifier(Verifier.Context context) { var manifest = context.Manifest; var arrayOfOne = new Json[1]; foreach (var (checkId, locationFunc, requirement) in k_LocationChecks) { try { var locations = locationFunc(manifest); var enumerable = locations as IEnumerable; if (enumerable == null) { // If locations is not an IEnumerable, it should be a single Json element (or null). arrayOfOne[0] = (Json)locations; enumerable = arrayOfOne; } foreach (var location in enumerable) { if (location == null) continue; if (!requirement.Func(location)) { context.AddError(checkId, $"{location.Path}: {requirement.Message}"); } } } catch (SimpleJsonException e) { context.AddError(checkId, e.Message); } } } public void CheckItem(Verifier.PackageFile file, int passIndex) { throw new InvalidOperationException(); } public void Finish() { } } }