mirror of
https://github.com/ScoopInstaller/Scoop.git
synced 2025-10-30 06:07:56 +00:00
[WIP] Validate manifests against JSON Schema in CI Tests (#1331)
* Add suggest and psmodule to schema.json * Fix required fields in schema.json * Improve url validation in schema.json * Add validator.exe as a single file validation tool * Add Scoop.Validator Lib for use in Manifest-Tests * Add buildscript for Scoop.Validator and validator.exe * Exclude .dll and packages folder from Project-Tests * Validate manifests against JSON Schema in CI Tests * Complete JSON Schema Validation * Dlls shouldn't be treated as text
This commit is contained in:
1
.gitattributes
vendored
1
.gitattributes
vendored
@@ -2,3 +2,4 @@
|
||||
* text eol=crlf
|
||||
*.exe -text
|
||||
*.zip -text
|
||||
*.dll -text
|
||||
|
||||
48
schema.json
48
schema.json
@@ -100,14 +100,23 @@
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"md5",
|
||||
"sha1",
|
||||
"sha256",
|
||||
"sha512"
|
||||
]
|
||||
},
|
||||
"url": {
|
||||
"anyOf": [
|
||||
{
|
||||
"format": "uri",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"pattern": "^\\$url.[\\w\\d]+$",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
@@ -190,8 +199,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"minItems": 1,
|
||||
"type": "array",
|
||||
"uniqueItems": true
|
||||
"type": "array"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -236,16 +244,21 @@
|
||||
"anyOf": [
|
||||
{
|
||||
"format": "uri",
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"not": {
|
||||
"pattern": "(\\$)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"items": {
|
||||
"format": "uri",
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"not": {
|
||||
"pattern": "(\\$)"
|
||||
}
|
||||
},
|
||||
"minItems": 1,
|
||||
"type": "array",
|
||||
"uniqueItems": true
|
||||
"type": "array"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -413,8 +426,29 @@
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
},
|
||||
"suggest": {
|
||||
"additionalProperties": false,
|
||||
"patternProperties": {
|
||||
"^(.*)$": {
|
||||
"$ref": "#/definitions/stringOrArrayOfStrings"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"psmodule": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"title": "scoop app manifest schema",
|
||||
"type": "object"
|
||||
"type": "object",
|
||||
"required": [
|
||||
"version"
|
||||
]
|
||||
}
|
||||
|
||||
1
supporting/validator/.gitignore
vendored
Normal file
1
supporting/validator/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
packages/
|
||||
BIN
supporting/validator/Newtonsoft.Json.Schema.dll
Normal file
BIN
supporting/validator/Newtonsoft.Json.Schema.dll
Normal file
Binary file not shown.
BIN
supporting/validator/Newtonsoft.Json.dll
Normal file
BIN
supporting/validator/Newtonsoft.Json.dll
Normal file
Binary file not shown.
126
supporting/validator/Scoop.Validator.cs
Normal file
126
supporting/validator/Scoop.Validator.cs
Normal file
@@ -0,0 +1,126 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Newtonsoft.Json.Schema;
|
||||
|
||||
namespace Scoop
|
||||
{
|
||||
public class JsonParserException : Exception
|
||||
{
|
||||
public string FileName { get; set; }
|
||||
public JsonParserException(string file, string message) : base(message) { this.FileName = file; }
|
||||
public JsonParserException(string file, string message, Exception inner) : base(message, inner) { this.FileName = file; }
|
||||
}
|
||||
|
||||
public class Validator
|
||||
{
|
||||
private bool CI { get; set; }
|
||||
public JSchema Schema { get; private set; }
|
||||
public FileInfo SchemaFile { get; private set; }
|
||||
public JObject Manifest { get; private set; }
|
||||
public FileInfo ManifestFile { get; private set; }
|
||||
public IList<string> Errors { get; private set; }
|
||||
public string ErrorsAsString
|
||||
{
|
||||
get
|
||||
{
|
||||
return String.Join(System.Environment.NewLine, this.Errors);
|
||||
}
|
||||
}
|
||||
|
||||
private JSchema ParseSchema(string file)
|
||||
{
|
||||
try
|
||||
{
|
||||
return JSchema.Parse(File.ReadAllText(file, System.Text.Encoding.UTF8));
|
||||
}
|
||||
catch (Newtonsoft.Json.JsonReaderException e)
|
||||
{
|
||||
throw new JsonParserException(Path.GetFileName(file), e.Message, e);
|
||||
}
|
||||
catch (FileNotFoundException e)
|
||||
{
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private JObject ParseManifest(string file)
|
||||
{
|
||||
try
|
||||
{
|
||||
return JObject.Parse(File.ReadAllText(file, System.Text.Encoding.UTF8));
|
||||
}
|
||||
catch (Newtonsoft.Json.JsonReaderException e)
|
||||
{
|
||||
throw new JsonParserException(Path.GetFileName(file), e.Message, e);
|
||||
}
|
||||
catch (FileNotFoundException e)
|
||||
{
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public Validator(string schemaFile)
|
||||
{
|
||||
this.SchemaFile = new FileInfo(schemaFile);
|
||||
this.Errors = new List<string>();
|
||||
}
|
||||
|
||||
public Validator(string schemaFile, bool ci)
|
||||
{
|
||||
this.SchemaFile = new FileInfo(schemaFile);
|
||||
this.Errors = new List<string>();
|
||||
this.CI = ci;
|
||||
}
|
||||
|
||||
public bool Validate(string file)
|
||||
{
|
||||
this.ManifestFile = new FileInfo(file);
|
||||
return this.Validate();
|
||||
}
|
||||
|
||||
public bool Validate()
|
||||
{
|
||||
this.Errors.Clear();
|
||||
try
|
||||
{
|
||||
if (this.Schema == null)
|
||||
{
|
||||
this.Schema = this.ParseSchema(this.SchemaFile.FullName);
|
||||
}
|
||||
this.Manifest = this.ParseManifest(this.ManifestFile.FullName);
|
||||
}
|
||||
catch (FileNotFoundException e)
|
||||
{
|
||||
this.Errors.Add(e.Message);
|
||||
}
|
||||
catch (JsonParserException e)
|
||||
{
|
||||
this.Errors.Add(String.Format("{0}{1}: {2}", (this.CI ? " [*] " : ""), e.FileName, e.Message));
|
||||
}
|
||||
|
||||
if (this.Schema == null || this.Manifest == null)
|
||||
return false;
|
||||
|
||||
IList<ValidationError> validationErrors = new List<ValidationError>();
|
||||
|
||||
this.Manifest.IsValid(this.Schema, out validationErrors);
|
||||
|
||||
if (validationErrors.Count > 0)
|
||||
{
|
||||
foreach (ValidationError error in validationErrors)
|
||||
{
|
||||
this.Errors.Add(String.Format("{0}{1}: {2}", (this.CI ? " [*] " : ""), this.ManifestFile.Name, error.Message));
|
||||
foreach (ValidationError childError in error.ChildErrors)
|
||||
{
|
||||
this.Errors.Add(String.Format((this.CI ? " [^] {0}{1}" : "{0}^ {1}"), new String(' ', this.ManifestFile.Name.Length + 2), childError.Message));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (this.Errors.Count == 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
supporting/validator/Scoop.Validator.dll
Normal file
BIN
supporting/validator/Scoop.Validator.dll
Normal file
Binary file not shown.
8
supporting/validator/build.ps1
Normal file
8
supporting/validator/build.ps1
Normal file
@@ -0,0 +1,8 @@
|
||||
$fwdir = gci C:\Windows\Microsoft.NET\Framework\ -dir | sort -desc | select -first 1
|
||||
|
||||
pushd $psscriptroot
|
||||
& nuget restore -solutiondirectory .
|
||||
gci $psscriptroot\packages\Newtonsoft.*\lib\net40\*.dll -file | % { copy-item $_ $psscriptroot }
|
||||
& "$($fwdir.fullname)\csc.exe" /platform:anycpu /nologo /optimize /target:library /reference:Newtonsoft.Json.dll,Newtonsoft.Json.Schema.dll Scoop.Validator.cs
|
||||
& "$($fwdir.fullname)\csc.exe" /platform:anycpu /nologo /optimize /target:exe /reference:Scoop.Validator.dll,Newtonsoft.Json.dll,Newtonsoft.Json.Schema.dll validator.cs
|
||||
popd
|
||||
5
supporting/validator/packages.config
Normal file
5
supporting/validator/packages.config
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="Newtonsoft.Json" version="9.0.1" targetFramework="net45" />
|
||||
<package id="Newtonsoft.Json.Schema" version="2.0.8" targetFramework="net45" />
|
||||
</packages>
|
||||
37
supporting/validator/validator.cs
Normal file
37
supporting/validator/validator.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace Scoop
|
||||
{
|
||||
public class Program
|
||||
{
|
||||
public static int Main(string[] args)
|
||||
{
|
||||
bool ci = (args.Length == 3 && args[2] == "-ci");
|
||||
bool valid = false;
|
||||
|
||||
if (args.Length < 2)
|
||||
{
|
||||
Console.WriteLine("Usage: validator.exe schema.json manifest.json");
|
||||
return 1;
|
||||
}
|
||||
|
||||
Scoop.Validator validator = new Scoop.Validator(args[0], ci);
|
||||
valid = validator.Validate(args[1]);
|
||||
|
||||
if (valid)
|
||||
{
|
||||
Console.WriteLine("Yay! {0} validates against the schema!", Path.GetFileName(args[1]));
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var error in validator.Errors)
|
||||
{
|
||||
Console.WriteLine(error);
|
||||
}
|
||||
}
|
||||
|
||||
return valid ? 0 : 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
supporting/validator/validator.exe
Normal file
BIN
supporting/validator/validator.exe
Normal file
Binary file not shown.
@@ -4,7 +4,8 @@ $repo_files = @( Get-ChildItem $repo_dir -file -recurse -force )
|
||||
|
||||
$project_file_exclusions = @(
|
||||
$([regex]::Escape($repo_dir.fullname)+'\\.git\\.*$'),
|
||||
'.sublime-workspace$'
|
||||
'.sublime-workspace$',
|
||||
'supporting\\validator\\packages\\*'
|
||||
)
|
||||
|
||||
describe 'Project code' {
|
||||
@@ -78,7 +79,7 @@ describe 'Style constraints for non-binary project files' {
|
||||
# gather all files except '*.exe', '*.zip', or any .git repository files
|
||||
$repo_files |
|
||||
where-object { $_.fullname -inotmatch $($project_file_exclusions -join '|') } |
|
||||
where-object { $_.fullname -inotmatch '(.exe|.zip)$' }
|
||||
where-object { $_.fullname -inotmatch '(.exe|.zip|.dll)$' }
|
||||
)
|
||||
|
||||
$files_exist = ($files.Count -gt 0)
|
||||
|
||||
@@ -3,41 +3,68 @@
|
||||
. "$psscriptroot\..\lib\manifest.ps1"
|
||||
|
||||
describe "manifest-validation" {
|
||||
beforeall {
|
||||
$working_dir = setup_working "manifest"
|
||||
$schema = "$psscriptroot\..\schema.json"
|
||||
Add-Type -Path "$psscriptroot\..\supporting\validator\Newtonsoft.Json.dll"
|
||||
Add-Type -Path "$psscriptroot\..\supporting\validator\Newtonsoft.Json.Schema.dll"
|
||||
Add-Type -Path "$psscriptroot\..\supporting\validator\Scoop.Validator.dll"
|
||||
}
|
||||
|
||||
it "Scoop.Validator is available" {
|
||||
([System.Management.Automation.PSTypeName]'Scoop.Validator').Type | should be 'Scoop.Validator'
|
||||
}
|
||||
|
||||
context "parse_json function" {
|
||||
it "fails with invalid json" {
|
||||
{ parse_json "$working_dir\broken_wget.json" } | should throw
|
||||
}
|
||||
}
|
||||
|
||||
context "schema validation" {
|
||||
it "fails with broken schema" {
|
||||
$validator = new-object Scoop.Validator("$working_dir\broken_schema.json", $true)
|
||||
$validator.Validate("$working_dir\wget.json") | should be $false
|
||||
$validator.Errors.Count | should be 1
|
||||
$validator.Errors | select-object -First 1 | should belikeexactly "*broken_schema.json*Path 'type', line 6, position 4."
|
||||
}
|
||||
it "fails with broken manifest" {
|
||||
$validator = new-object Scoop.Validator($schema, $true)
|
||||
$validator.Validate("$working_dir\broken_wget.json") | should be $false
|
||||
$validator.Errors.Count | should be 1
|
||||
$validator.Errors | select-object -First 1 | should belikeexactly "*broken_wget.json*Path 'version', line 5, position 4."
|
||||
}
|
||||
it "fails with invalid manifest" {
|
||||
$validator = new-object Scoop.Validator($schema, $true)
|
||||
$validator.Validate("$working_dir\invalid_wget.json") | should be $false
|
||||
$validator.Errors.Count | should be 10
|
||||
$validator.Errors | select-object -First 1 | should belikeexactly "*invalid_wget.json*randomproperty*"
|
||||
$validator.Errors | select-object -Last 1 | should belikeexactly "*invalid_wget.json*version."
|
||||
}
|
||||
}
|
||||
|
||||
context "manifest validates against the schema" {
|
||||
beforeall {
|
||||
$bucketdir = "$psscriptroot\..\bucket\"
|
||||
$manifest_files = gci $bucketdir *.json
|
||||
|
||||
$validator = new-object Scoop.Validator($schema, $true)
|
||||
}
|
||||
$manifest_files | % {
|
||||
it "test validity of $_" {
|
||||
$manifest = parse_json $_.fullname
|
||||
it "$_" {
|
||||
$validator.Validate($_.fullname)
|
||||
if ($validator.Errors.Count -gt 0) {
|
||||
write-host -f yellow $validator.ErrorsAsString
|
||||
}
|
||||
$validator.Errors.Count | should be 0
|
||||
|
||||
$manifest = parse_json $_.fullname
|
||||
$url = arch_specific "url" $manifest "32bit"
|
||||
$url | should not match "\$"
|
||||
$url64 = arch_specific "url" $manifest "64bit"
|
||||
$url64 | should not match "\$"
|
||||
if(!$url) {
|
||||
$url = $url64
|
||||
}
|
||||
$url | should not benullorempty
|
||||
|
||||
$extract_dir32 = arch_specific "extract_dir" $manifest "32bit"
|
||||
$extract_dir32 | should not match "\$"
|
||||
$extract_dir64 = arch_specific "extract_dir" $manifest "64bit"
|
||||
$extract_dir64 | should not match "\$"
|
||||
|
||||
$manifest | should not benullorempty
|
||||
$manifest.version | should not benullorempty
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe "parse_json" {
|
||||
beforeall {
|
||||
$working_dir = setup_working "parse_json"
|
||||
}
|
||||
|
||||
context "json is invalid" {
|
||||
it "fails with invalid json" {
|
||||
{ parse_json "$working_dir\wget.json" } | should throw
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
11
test/fixtures/manifest/broken_schema.json
vendored
Normal file
11
test/fixtures/manifest/broken_schema.json
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"$id": "http://scoop.sh/draft/schema#",
|
||||
"$schema": "http://scoop.sh/draft/schema#",
|
||||
"title": "scoop app manifest schema",
|
||||
"type": "object"
|
||||
"properties": {
|
||||
"version": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
29
test/fixtures/manifest/invalid_wget.json
vendored
Normal file
29
test/fixtures/manifest/invalid_wget.json
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"homepage": "https://eternallybored.org/misc/wget/",
|
||||
"randomproperty": "should fail",
|
||||
"license": "GPL3",
|
||||
"architecture": {
|
||||
"64bit": {
|
||||
"url": [
|
||||
"https://eternallybored.org/misc/wget/wget-$version-win64.zip",
|
||||
"http://curl.haxx.se/ca/cacert.pem"
|
||||
],
|
||||
"hash": [
|
||||
"85e5393ffd473f7bec40b57637fd09b6808df86c06f846b6885b261a8acac8c5",
|
||||
""
|
||||
]
|
||||
},
|
||||
"32bit": {
|
||||
"url": [
|
||||
"https://eternallybored.org/misc/wget/wget-$version-win32.zip",
|
||||
"http://curl.haxx.se/ca/cacert.pem"
|
||||
],
|
||||
"hash": [
|
||||
"2ef82af3070abfdaf3862baff0bffdcb3c91c8d75e2f02c8720d90adb9d7a8f7",
|
||||
""
|
||||
]
|
||||
}
|
||||
},
|
||||
"bin": "wget.exe",
|
||||
"post_install": "\"ca_certificate=$dir\\cacert.pem\" | out-file $dir\\wget.ini -encoding default"
|
||||
}
|
||||
29
test/fixtures/manifest/wget.json
vendored
Normal file
29
test/fixtures/manifest/wget.json
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"homepage": "https://eternallybored.org/misc/wget/",
|
||||
"license": "GPL3",
|
||||
"version": "1.16.3",
|
||||
"architecture": {
|
||||
"64bit": {
|
||||
"url": [
|
||||
"https://eternallybored.org/misc/wget/wget-1.16.3-win64.zip",
|
||||
"http://curl.haxx.se/ca/cacert.pem"
|
||||
],
|
||||
"hash": [
|
||||
"85e5393ffd473f7bec40b57637fd09b6808df86c06f846b6885b261a8acac8c5",
|
||||
""
|
||||
]
|
||||
},
|
||||
"32bit": {
|
||||
"url": [
|
||||
"https://eternallybored.org/misc/wget/wget-1.16.3-win32.zip",
|
||||
"http://curl.haxx.se/ca/cacert.pem"
|
||||
],
|
||||
"hash": [
|
||||
"2ef82af3070abfdaf3862baff0bffdcb3c91c8d75e2f02c8720d90adb9d7a8f7",
|
||||
""
|
||||
]
|
||||
}
|
||||
},
|
||||
"bin": "wget.exe",
|
||||
"post_install": "\"ca_certificate=$dir\\cacert.pem\" | out-file $dir\\wget.ini -encoding default"
|
||||
}
|
||||
Reference in New Issue
Block a user