using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using StardewModdingAPI.Toolkit.Utilities;
namespace StardewModdingAPI.Toolkit.Framework
{
/// Validates manifest fields.
public static class ManifestValidator
{
/// Validate a manifest's fields.
/// The manifest to validate.
/// The error message indicating why validation failed, if applicable.
/// Returns whether all manifest fields validated successfully.
[SuppressMessage("ReSharper", "ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract", Justification = "This is the method that ensures those annotations are respected.")]
public static bool TryValidateFields(IManifest manifest, out string error)
{
//
// Note: SMAPI assumes that it can grammatically append the returned sentence in the
// form "failed loading because its ". Any errors returned should be valid
// in that format, unless the SMAPI call is adjusted accordingly.
//
bool hasDll = !string.IsNullOrWhiteSpace(manifest.EntryDll);
bool isContentPack = manifest.ContentPackFor != null;
// validate use of EntryDll vs ContentPackFor fields
if (hasDll == isContentPack)
{
error = hasDll
? $"manifest sets both {nameof(IManifest.EntryDll)} and {nameof(IManifest.ContentPackFor)}, which are mutually exclusive."
: $"manifest has no {nameof(IManifest.EntryDll)} or {nameof(IManifest.ContentPackFor)} field; must specify one.";
return false;
}
// validate EntryDll/ContentPackFor format
if (hasDll)
{
if (manifest.EntryDll!.Intersect(Path.GetInvalidFileNameChars()).Any())
{
error = $"manifest has invalid filename '{manifest.EntryDll}' for the {nameof(IManifest.EntryDll)} field.";
return false;
}
}
else
{
if (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 dependency format
foreach (IManifestDependency? dependency in manifest.Dependencies)
{
if (dependency == null)
{
error = $"manifest has a null entry under {nameof(IManifest.Dependencies)}.";
return false;
}
if (string.IsNullOrWhiteSpace(dependency.UniqueID))
{
error = $"manifest has a {nameof(IManifest.Dependencies)} entry with no {nameof(IManifestDependency.UniqueID)} field.";
return false;
}
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;
}
}
}