diff --git a/src/SMAPI.ModBuildConfig/DeployModTask.cs b/src/SMAPI.ModBuildConfig/DeployModTask.cs index 357e02b5..70761a2f 100644 --- a/src/SMAPI.ModBuildConfig/DeployModTask.cs +++ b/src/SMAPI.ModBuildConfig/DeployModTask.cs @@ -9,6 +9,7 @@ using Microsoft.Build.Framework; using Microsoft.Build.Utilities; using Newtonsoft.Json; using StardewModdingAPI.ModBuildConfig.Framework; +using StardewModdingAPI.Toolkit.Framework; using StardewModdingAPI.Toolkit.Serialization; using StardewModdingAPI.Toolkit.Serialization.Models; using StardewModdingAPI.Toolkit.Utilities; @@ -91,7 +92,8 @@ namespace StardewModdingAPI.ModBuildConfig try { new JsonHelper().ReadJsonFileIfExists(manifestFile.FullName, out manifest); - } catch (JsonReaderException ex) + } + catch (JsonReaderException ex) { // log the inner exception, otherwise the message will be generic Exception exToShow = ex.InnerException ?? ex; @@ -100,7 +102,7 @@ namespace StardewModdingAPI.ModBuildConfig } // validate the manifest's fields - if (!manifest.TryValidate(out string error)) + if (!ManifestValidator.TryValidate(manifest, out string error)) { this.Log.LogError($"[mod build package] The mod manifest is invalid: {error}"); return false; diff --git a/src/SMAPI.Toolkit/Framework/ManifestValidator.cs b/src/SMAPI.Toolkit/Framework/ManifestValidator.cs new file mode 100644 index 00000000..62cfd8e9 --- /dev/null +++ b/src/SMAPI.Toolkit/Framework/ManifestValidator.cs @@ -0,0 +1,101 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using StardewModdingAPI.Toolkit.Utilities; + +namespace StardewModdingAPI.Toolkit.Framework +{ + /// Validates manifest fields. + public static class ManifestValidator + { + /// Try to validate a manifest's fields. Fails if any invalid field is found. + /// The manifest to validate. + /// The error message to display to the user. + /// Returns whether the manifest was validated successfully. + public static bool TryValidate(IManifest manifest, out string error) + { + // validate DLL / content pack fields + bool hasDll = !string.IsNullOrWhiteSpace(manifest.EntryDll); + bool isContentPack = manifest.ContentPackFor != null; + + // validate field presence + if (!hasDll && !isContentPack) + { + error = $"manifest has no {nameof(IManifest.EntryDll)} or {nameof(IManifest.ContentPackFor)} field; must specify one."; + return false; + } + if (hasDll && isContentPack) + { + error = $"manifest sets both {nameof(IManifest.EntryDll)} and {nameof(IManifest.ContentPackFor)}, which are mutually exclusive."; + return false; + } + + // validate DLL filename format + if (hasDll && manifest.EntryDll!.Intersect(Path.GetInvalidFileNameChars()).Any()) + { + error = $"manifest has invalid filename '{manifest.EntryDll}' for the EntryDLL field."; + return false; + } + + // validate content pack ID + else if (isContentPack && string.IsNullOrWhiteSpace(manifest.ContentPackFor!.UniqueID)) + { + error = $"manifest declares {nameof(IManifest.ContentPackFor)} without its required {nameof(IManifestContentPackFor.UniqueID)} field."; + return false; + } + + // validate required fields + { + List missingFields = new List(3); + + if (string.IsNullOrWhiteSpace(manifest.Name)) + missingFields.Add(nameof(IManifest.Name)); + if (manifest.Version == null || manifest.Version.ToString() == "0.0.0") + missingFields.Add(nameof(IManifest.Version)); + if (string.IsNullOrWhiteSpace(manifest.UniqueID)) + missingFields.Add(nameof(IManifest.UniqueID)); + + if (missingFields.Any()) + { + error = $"manifest is missing required fields ({string.Join(", ", missingFields)})."; + return false; + } + } + + // validate ID format + if (!PathUtilities.IsSlug(manifest.UniqueID)) + { + error = "manifest specifies an invalid ID (IDs must only contain letters, numbers, underscores, periods, or hyphens)."; + return false; + } + + // validate dependencies + foreach (IManifestDependency? dependency in manifest.Dependencies) + { + // null dependency + if (dependency == null) + { + error = $"manifest has a null entry under {nameof(IManifest.Dependencies)}."; + return false; + } + + // missing ID + if (string.IsNullOrWhiteSpace(dependency.UniqueID)) + { + error = $"manifest has a {nameof(IManifest.Dependencies)} entry with no {nameof(IManifestDependency.UniqueID)} field."; + return false; + } + + // invalid ID + if (!PathUtilities.IsSlug(dependency.UniqueID)) + { + error = $"manifest has a {nameof(IManifest.Dependencies)} entry with an invalid {nameof(IManifestDependency.UniqueID)} field (IDs must only contain letters, numbers, underscores, periods, or hyphens)."; + return false; + } + } + + error = ""; + return true; + } + } +} diff --git a/src/SMAPI.Toolkit/Serialization/Models/Manifest.cs b/src/SMAPI.Toolkit/Serialization/Models/Manifest.cs index 1607cf3e..8a449f0a 100644 --- a/src/SMAPI.Toolkit/Serialization/Models/Manifest.cs +++ b/src/SMAPI.Toolkit/Serialization/Models/Manifest.cs @@ -1,12 +1,9 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Linq; using System.Text; using Newtonsoft.Json; using StardewModdingAPI.Toolkit.Serialization.Converters; -using StardewModdingAPI.Toolkit.Utilities; namespace StardewModdingAPI.Toolkit.Serialization.Models { @@ -106,95 +103,6 @@ namespace StardewModdingAPI.Toolkit.Serialization.Models this.UpdateKeys = updateKeys ?? Array.Empty(); } - /// Try to validate a manifest's fields. Fails if any invalid field is found. - /// The error message to display to the user. - /// Returns whether the manifest was validated successfully. - public bool TryValidate(out string error) - { - // validate DLL / content pack fields - bool hasDll = !string.IsNullOrWhiteSpace(this.EntryDll); - bool isContentPack = this.ContentPackFor != null; - - // validate field presence - if (!hasDll && !isContentPack) - { - error = $"manifest has no {nameof(IManifest.EntryDll)} or {nameof(IManifest.ContentPackFor)} field; must specify one."; - return false; - } - if (hasDll && isContentPack) - { - error = $"manifest sets both {nameof(IManifest.EntryDll)} and {nameof(IManifest.ContentPackFor)}, which are mutually exclusive."; - return false; - } - - // validate DLL filename format - if (hasDll && this.EntryDll!.Intersect(Path.GetInvalidFileNameChars()).Any()) - { - error = $"manifest has invalid filename '{this.EntryDll}' for the EntryDLL field."; - return false; - } - - // validate content pack ID - else if (isContentPack && string.IsNullOrWhiteSpace(this.ContentPackFor!.UniqueID)) - { - error = $"manifest declares {nameof(IManifest.ContentPackFor)} without its required {nameof(IManifestContentPackFor.UniqueID)} field."; - return false; - } - - // validate required fields - { - List missingFields = new List(3); - - if (string.IsNullOrWhiteSpace(this.Name)) - missingFields.Add(nameof(IManifest.Name)); - if (this.Version == null || this.Version.ToString() == "0.0.0") - missingFields.Add(nameof(IManifest.Version)); - if (string.IsNullOrWhiteSpace(this.UniqueID)) - missingFields.Add(nameof(IManifest.UniqueID)); - - if (missingFields.Any()) - { - error = $"manifest is missing required fields ({string.Join(", ", missingFields)})."; - return false; - } - } - - // validate ID format - if (!PathUtilities.IsSlug(this.UniqueID)) - { - error = "manifest specifies an invalid ID (IDs must only contain letters, numbers, underscores, periods, or hyphens)."; - return false; - } - - // validate dependencies - foreach (IManifestDependency? dependency in this.Dependencies) - { - // null dependency - if (dependency == null) - { - error = $"manifest has a null entry under {nameof(IManifest.Dependencies)}."; - return false; - } - - // missing ID - if (string.IsNullOrWhiteSpace(dependency.UniqueID)) - { - error = $"manifest has a {nameof(IManifest.Dependencies)} entry with no {nameof(IManifestDependency.UniqueID)} field."; - return false; - } - - // invalid ID - if (!PathUtilities.IsSlug(dependency.UniqueID)) - { - error = $"manifest has a {nameof(IManifest.Dependencies)} entry with an invalid {nameof(IManifestDependency.UniqueID)} field (IDs must only contain letters, numbers, underscores, periods, or hyphens)."; - return false; - } - } - - error = ""; - return true; - } - /// Override the update keys loaded from the mod info. /// The new update keys to set. internal void OverrideUpdateKeys(params string[] updateKeys) diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index 352c22cc..0b4fe3e9 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -4,6 +4,7 @@ using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using StardewModdingAPI.Toolkit; +using StardewModdingAPI.Toolkit.Framework; using StardewModdingAPI.Toolkit.Framework.ModData; using StardewModdingAPI.Toolkit.Framework.ModScanning; using StardewModdingAPI.Toolkit.Framework.UpdateData; @@ -138,7 +139,7 @@ namespace StardewModdingAPI.Framework.ModLoading } // validate manifest - if (mod.Manifest is Manifest manifest && !manifest.TryValidate(out string manifestError)) + if (!ManifestValidator.TryValidate(mod.Manifest, out string manifestError)) { mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its {manifestError}"); continue;