diff --git a/Core/CKAN-core.csproj b/Core/CKAN-core.csproj index 69f7596df7..8858b5b317 100644 --- a/Core/CKAN-core.csproj +++ b/Core/CKAN-core.csproj @@ -53,7 +53,11 @@ ..\_build\lib\nuget\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll + + ..\_build\lib\nuget\NJsonSchema.9.10.6\lib\net45\NJsonSchema.dll + + @@ -132,10 +136,12 @@ + + diff --git a/Core/CKAN.schema b/Core/CKAN.schema new file mode 100644 index 0000000000..3aab11ee07 --- /dev/null +++ b/Core/CKAN.schema @@ -0,0 +1,423 @@ +{ + "$schema" : "http://json-schema.org/draft-04/schema#", + "title" : "CKAN JSON Schema", + "description" : "Schema for validating CKAN meta files", + "type" : "object", + "properties" : { + "spec_version" : { + "description" : "Version of the CKAN spec this document uses", + "oneOf" : [ + { + "type" : "integer", + "description" : "The integer '1' is special case", + "minimum" : 1, + "maximum" : 1 + }, + { + "type" : "string", + "description" : "A vx.x string", + "pattern" : "^v[0-9]+\\.[0-9]+$" + } + ] + }, + "name" : { + "description" : "Human readable name of the mod", + "type" : "string" + }, + "identifier" : { + "description" : "Unique identifier for this mod.", + "$ref" : "#/definitions/identifier" + }, + "kind" : { + "description" : "Package type, defaults to package.", + "enum" : [ "package", "metapackage" ] + }, + "abstract" : { + "description" : "Short description of the mod", + "type" : "string" + }, + "description" : { + "description" : "Longer description of the mod", + "type" : "string" + }, + "comment" : { + "description" : "Free-form comment. Not displayed to users.", + "type" : "string" + }, + "author" : { + "description" : "Author, or list of authors.", + "oneOf" : [ + { "type" : "string" }, + { + "type" : "array", + "items" : { "type" : "string" }, + "uniqueItems" : true + } + ] + }, + "download" : { + "description" : "URL where mod can be downloaded by tools", + "type" : "string", + "format" : "uri" + }, + "download_size" : { + "description" : "The size of the download in bytes", + "type" : "integer" + }, + "download_hash" : { + "description" : "A object of hashes of the downloaded file", + "type" : "object", + "properties" : { + "sha1" : { + "description" : "SHA1 hash of the file", + "type" : "string" + }, + "sha256" : { + "description" : "SHA256 hash of the file", + "type" : "string" + } + } + }, + "download_content_type" : { + "description" : "The content type of the download", + "type" : "string" + }, + "license" : { + "description" : "Machine readable license, or array of licenses", + "$ref" : "#/definitions/licenses" + }, + "version" : { + "description" : "Version of the mod. Drop the leading v", + "$ref" : "#/definitions/version" + }, + "release_status" : { + "description" : "Optional release status", + "enum" : [ "stable", "testing", "development" ] + }, + "ksp_version" : { + "description" : "Optional target KSP version", + "$ref" : "#/definitions/ksp_version" + }, + "ksp_version_min" : { + "description" : "Optional minimum KSP version", + "$ref" : "#/definitions/ksp_version" + }, + "ksp_version_max" : { + "description" : "Optional maximum KSP version", + "$ref" : "#/definitions/ksp_version" + }, + "ksp_version_strict" : { + "description" : "Optional enforcement of strict version checks (defaults to false)", + "type" : "boolean" + }, + "tags" : { + "description" : "A series of descriptive keywords to provide quick filtering and organization", + "type" : "array", + "items" : { + "type" : "string", + "pattern" : "^[a-z0-9-]+$" + } + }, + "depends" : { + "description" : "Optional list of dependencies", + "$ref" : "#/definitions/relationship" + }, + "recommends" : { + "description" : "Optional list of recommended mods", + "$ref" : "#/definitions/relationship" + }, + "suggests" : { + "description" : "Optional list of recommended, but not essential mods", + "$ref" : "#/definitions/relationship" + }, + "supports" : { + "description" : "Optional list of supported mods", + "$ref" : "#/definitions/relationship" + }, + "conflicts" : { + "description" : "Optional list of conflicting mods", + "$ref" : "#/definitions/relationship" + }, + "provides" : { + "description" : "A list of virtual packages this mod provides", + "type" : "array", + "items" : { "type" : "string" }, + "uniqueItems" : true + }, + "resources" : { + "description" : "Additional resources", + "type" : "object", + "properties" : { + "homepage" : { + "description" : "Mod homepage", + "type" : "string", + "format" : "uri" + }, + "bugtracker" : { + "description" : "Mod bugtracker", + "type" : "string", + "format" : "uri" + }, + "license" : { + "description" : "Mod license", + "type" : "string", + "format" : "uri" + }, + "repository" : { + "description" : "Mod repository", + "type" : "string", + "format" : "uri" + }, + "ci" : { + "description" : "Continuous Integration", + "type" : "string", + "format" : "uri" + }, + "spacedock" : { + "description" : "Project on SpaceDock", + "type" : "string", + "format" : "uri" + }, + "curse" : { + "description" : "Project on Curse", + "type" : "string", + "format" : "uri" + }, + "manual" : { + "description" : "Mod's manual", + "type" : "string", + "format" : "uri" + } + } + }, + "install" : { + "description" : "List of install directives", + "type" : "array", + "items" : { + "type" : "object", + "properties" : { + "file" : { + "description" : "Path to file to install", + "type" : "string" + }, + "find" : { + "description" : "A directory to find for installation", + "type" : "string" + }, + "find_regexp" : { + "description" : "A regexp that matches the directory to install", + "type" : "string" + }, + "find_matches_files" : { + "description" : "If true, find directives match files as well as directories", + "type" : "boolean" + }, + "install_to" : { + "description" : "Where file should be installed to", + "$ref" : "#/definitions/install_to" + }, + "as": { + "description" : "The name to give the matching directory or file when installed", + "type" : "string", + "pattern" : "^[^\\\\/]+$" + }, + "filter" : { + "description" : "List of files and directories that should be filtered from the install", + "$ref" : "#/definitions/one_or_more_strings" + }, + "filter_regexp" : { + "description" : "List of regexps that should filter files from this install", + "$ref" : "#/definitions/one_or_more_strings" + }, + "include_only" : { + "description" : "List of files and directories that should not be excluded from the install", + "$ref" : "#/definitions/one_or_more_strings" + }, + "include_only_regexp" : { + "description" : "List of regexps that should include files in this install", + "$ref" : "#/definitions/one_or_more_strings" + } + }, + "not" : { + "anyOf" : [ + { + "required" : [ "filter", "include_only" ] + }, + { + "required" : [ "filter", "include_only_regexp" ] + }, + { + "required" : [ "filter_regexp", "include_only" ] + }, + { + "required" : [ "filter_regexp", "include_only_regexp" ] + } + ] + }, + "required" : [ "install_to" ], + "oneOf": [ + { + "required": [ "file" ] + }, + { + "required": [ "find" ] + }, + { + "required": [ "find_regexp" ] + } + ] + } + } + }, + "required" : [ + "spec_version", + "name", + "abstract", + "identifier", + "license", + "version" + ], + "oneOf": [ + { + "required": [ "download" ] + }, + { + "properties": { + "kind": { + "enum": [ "metapackage" ] + } + }, + "not": { + "required": [ "download" ] + }, + "required": [ "kind" ] + } + ], + "definitions" : { + "identifier" : { + "description" : "Unique identifier for this mod.", + "type" : "string", + "pattern" : "^[A-Za-z0-9-]*$" + }, + "version" : { + "description" : "A version string", + "type" : "string" + }, + "ksp_version" : { + "description" : "A version of KSP", + "type" : "string", + "pattern" : "^(any|[0-9]+\\.[0-9]+(\\.[0-9]+)?)$" + }, + "relationship" : { + "description" : "A relationship to a list of mods", + "type" : "array", + "items" : { + "type" : "object", + "properties" : { + "name" : { + "description" : "Identifier of the mod", + "$ref" : "#/definitions/identifier" + }, + "version" : { + "description" : "Optional version", + "$ref" : "#/definitions/version" + }, + "min_version" : { + "description" : "Optional minimum version", + "$ref" : "#/definitions/version" + }, + "max_version" : { + "description" : "Optional maximum version", + "$ref" : "#/definitions/version" + } + }, + "required" : [ "name" ] + } + }, + "install_to" : { + "description" : "Where file should be installed to. As of v1.2, GameData may take a path", + "oneOf" : [ + { + "description" : "Spec version 1 targets", + "enum" : [ "GameData", "Ships", "GameRoot", "Tutorial", "Scenarios" ] + }, + { + "description" : "Spec v1.2 GameData path", + "type" : "string", + "pattern" : "^GameData/" + }, + { + "description" : "Spec v1.12 Ships subfolder paths", + "type" : "string", + "pattern" : "^Ships/(SPH|VAB)$" + }, + { + "description" : "Spec v1.16 thumbs paths", + "type" : "string", + "pattern" : "^Ships/@thumbs/(SPH|VAB)$" + } + ] + }, + "license" : { + "description" : "A license.", + "enum" : [ + "public-domain", + "AFL-3.0", + "AGPL-3.0", + "Apache", "Apache-1.0", "Apache-2.0", + "APSL-2.0", + "Artistic", "Artistic-1.0", "Artistic-2.0", + "BSD-2-clause", "BSD-3-clause", "BSD-4-clause", + "ISC", + "CC-BY", "CC-BY-1.0", "CC-BY-2.0", "CC-BY-2.5", "CC-BY-3.0", "CC-BY-4.0", + "CC-BY-SA", "CC-BY-SA-1.0", "CC-BY-SA-2.0", "CC-BY-SA-2.5", "CC-BY-SA-3.0", "CC-BY-SA-4.0", + "CC-BY-NC", "CC-BY-NC-1.0", "CC-BY-NC-2.0", "CC-BY-NC-2.5", "CC-BY-NC-3.0", "CC-BY-NC-4.0", + "CC-BY-NC-SA", "CC-BY-NC-SA-1.0", "CC-BY-NC-SA-2.0", "CC-BY-NC-SA-2.5", "CC-BY-NC-SA-3.0", "CC-BY-NC-SA-4.0", + "CC-BY-NC-ND", "CC-BY-NC-ND-1.0", "CC-BY-NC-ND-2.0", "CC-BY-NC-ND-2.5", "CC-BY-NC-ND-3.0", "CC-BY-NC-ND-4.0", + "CC-BY-ND", "CC-BY-ND-1.0", "CC-BY-ND-2.0", "CC-BY-ND-2.5", "CC-BY-ND-3.0", "CC-BY-ND-4.0", + "CC0", + "CDDL", "CPL", + "EFL-1.0", "EFL-2.0", + "Expat", "MIT", + "GPL-1.0", "GPL-2.0", "GPL-3.0", + "LGPL-2.0", "LGPL-2.1", "LGPL-3.0", + "GFDL-1.0", "GFDL-1.1", "GFDL-1.2", "GFDL-1.3", + "GFDL-NIV-1.0", "GFDL-NIV-1.1", "GFDL-NIV-1.2", "GFDL-NIV-1.3", + "LPPL-1.0", "LPPL-1.1", "LPPL-1.2", "LPPL-1.3c", + "MPL-1.0", "MPL-1.1", + "Ms-PL", "Ms-RL", + "Perl", + "Python-2.0", + "QPL-1.0", + "Unlicense", + "W3C", + "WTFPL", + "Zlib", + "Zope", + "open-source", "restricted", "unrestricted", "unknown" + ] + }, + "licenses" : { + "description" : "A license, or array of licenses", + "anyOf" : [ + { "$ref" : "#/definitions/license" }, + { + "type" : "array", + "items" : { "$ref" : "#/definitions/license" }, + "uniqueItems" : true + } + ] + }, + "one_or_more_strings" : { + "description" : "One or more strings", + "oneOf" : [ + { "type" : "string" }, + { + "type" : "array", + "items" : { "type" : "string" }, + "uniqueItems" : true + } + ] + } + } +} diff --git a/Core/Types/CkanModule.cs b/Core/Types/CkanModule.cs index 1467286ac0..4c52b7aff8 100644 --- a/Core/Types/CkanModule.cs +++ b/Core/Types/CkanModule.cs @@ -2,8 +2,11 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Reflection; using System.Runtime.Serialization; using System.Text.RegularExpressions; +using System.Threading.Tasks; +using NJsonSchema; using Autofac; using CKAN.Versioning; using log4net; @@ -292,6 +295,13 @@ public CkanModule(string json, IGameComparator comparator) { _comparator = comparator; + var errors = ValidateAgainstSchema(json); + if (errors.Result.Any()) + { + var message = "Validation against the schema failed.\n" + string.Join("\n ", errors); + throw new BadMetadataKraken(null, message); + } + try { // Use the json string to populate our object @@ -366,6 +376,33 @@ private void DeSerialisationFixes(StreamingContext like_i_could_care) name = name ?? string.Empty; } + static CkanModule() + { + var assembly = Assembly.GetExecutingAssembly(); + using (var stream = assembly.GetManifestResourceStream(SchemaPath)) + using (var reader = new StreamReader(stream)) + { + var result = reader.ReadToEnd(); + MetadataSchema = JsonSchema4.FromSampleJson(result); + } + } + + private static readonly string SchemaPath = "CKAN.CKAN.schema"; + private static readonly JsonSchema4 MetadataSchema; + + private static async Task> ValidateAgainstSchema(string json) + { + JsonSchema4 schema = await JsonSchema4.FromJsonAsync(json); + + IList errorList = new List(); + var errors = schema.Validate(MetadataSchema.ToJson()); + + foreach (var error in errors) + errorList.Add(error.ToString()); + + return errorList; + } + /// /// Tries to parse an identifier in the format Modname=version /// If the module cannot be found in the registry, throws a ModuleNotFoundKraken. diff --git a/Core/app.config b/Core/app.config new file mode 100644 index 0000000000..dde2c3cc64 --- /dev/null +++ b/Core/app.config @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/Core/packages.config b/Core/packages.config index aa696112f9..3edeb57499 100644 --- a/Core/packages.config +++ b/Core/packages.config @@ -4,5 +4,6 @@ + \ No newline at end of file