diff --git a/.github/actions/spelling/allow.txt b/.github/actions/spelling/allow.txt index 595ea15c4f..796b923545 100644 --- a/.github/actions/spelling/allow.txt +++ b/.github/actions/spelling/allow.txt @@ -261,6 +261,7 @@ OSVERSIONINFOEXW outfile OUTOFMEMORY OWC +PACKAGESSCHEMA Params parentidx pathpart diff --git a/.github/actions/spelling/patterns.txt b/.github/actions/spelling/patterns.txt index 6cf1760181..74ab869cb3 100644 --- a/.github/actions/spelling/patterns.txt +++ b/.github/actions/spelling/patterns.txt @@ -32,4 +32,4 @@ El proyecto .* diferentes http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer # schema regex -"pattern": .*$ \ No newline at end of file +"pattern": .*$ diff --git a/src/AppInstallerCLI.sln b/src/AppInstallerCLI.sln index 5a2fbaf601..2b0bfe49db 100644 --- a/src/AppInstallerCLI.sln +++ b/src/AppInstallerCLI.sln @@ -17,10 +17,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Project", "Project", "{8D53 ..\azure-pipelines.loc.yml = ..\azure-pipelines.loc.yml ..\azure-pipelines.yml = ..\azure-pipelines.yml ..\cgmanifest.json = ..\cgmanifest.json - ..\doc\packages.schema.json = ..\doc\packages.schema.json ..\README.md = ..\README.md ..\doc\Settings.md = ..\doc\Settings.md - ..\doc\settings.schema.json = ..\doc\settings.schema.json EndProjectSection EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "catch2", "catch2\catch2.vcxitems", "{5295E21E-9868-4DE2-A177-FBB97B36579B}" @@ -68,6 +66,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Valijson", "Valijson\Valijs EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ManifestSchema", "ManifestSchema\ManifestSchema.vcxitems", "{7D05F64D-CE5A-42AA-A2C1-E91458F061CF}" EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "WinGetSchemas", "WinGetSchemas\WinGetSchemas.vcxitems", "{952B513F-8A00-4D74-9271-925AFB3C6252}" +EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "spelling", "spelling", "{2ACDE176-F13F-42FA-8159-C34FA3D37837}" ProjectSection(SolutionItems) = preProject ..\.github\actions\spelling\allow.txt = ..\.github\actions\spelling\allow.txt @@ -79,16 +79,21 @@ EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution ManifestSchema\ManifestSchema.vcxitems*{1622da16-914f-4f57-a259-d5169003cc8c}*SharedItemsImports = 4 + Valijson\Valijson.vcxitems*{1c6e0108-2860-4b17-9f7e-fa5c6c1f3d3d}*SharedItemsImports = 4 + WinGetSchemas\WinGetSchemas.vcxitems*{1c6e0108-2860-4b17-9f7e-fa5c6c1f3d3d}*SharedItemsImports = 4 Valijson\Valijson.vcxitems*{358bc478-0624-4ad1-a933-0422b5292af8}*SharedItemsImports = 9 catch2\catch2.vcxitems*{5295e21e-9868-4de2-a177-fbb97b36579b}*SharedItemsImports = 9 ManifestSchema\ManifestSchema.vcxitems*{5890d6ed-7c3b-40f3-b436-b54f640d9e65}*SharedItemsImports = 4 Valijson\Valijson.vcxitems*{5890d6ed-7c3b-40f3-b436-b54f640d9e65}*SharedItemsImports = 4 binver\binver.vcxitems*{5b6f90df-fd19-4bae-83d9-24dad128e777}*SharedItemsImports = 4 ManifestSchema\ManifestSchema.vcxitems*{5b6f90df-fd19-4bae-83d9-24dad128e777}*SharedItemsImports = 4 + WinGetSchemas\WinGetSchemas.vcxitems*{5b6f90df-fd19-4bae-83d9-24dad128e777}*SharedItemsImports = 4 binver\binver.vcxitems*{6e36ddd7-1602-474e-b1d7-d0a7e1d5ad86}*SharedItemsImports = 9 ManifestSchema\ManifestSchema.vcxitems*{7d05f64d-ce5a-42aa-a2c1-e91458f061cf}*SharedItemsImports = 9 catch2\catch2.vcxitems*{89b1aab4-2bbc-4b65-9ed7-a01d5cf88230}*SharedItemsImports = 4 ManifestSchema\ManifestSchema.vcxitems*{89b1aab4-2bbc-4b65-9ed7-a01d5cf88230}*SharedItemsImports = 4 + WinGetSchemas\WinGetSchemas.vcxitems*{89b1aab4-2bbc-4b65-9ed7-a01d5cf88230}*SharedItemsImports = 4 + WinGetSchemas\WinGetSchemas.vcxitems*{952b513f-8a00-4d74-9271-925afb3c6252}*SharedItemsImports = 9 binver\binver.vcxitems*{fb313532-38b0-4676-9303-ab200aa13576}*SharedItemsImports = 4 ManifestSchema\ManifestSchema.vcxitems*{fb313532-38b0-4676-9303-ab200aa13576}*SharedItemsImports = 4 EndGlobalSection @@ -437,6 +442,7 @@ Global {3BAF989F-7F65-465B-ACE8-BAFE42D1017E} = {EA8CD934-0702-4911-A2C5-A40600E616DE} {358BC478-0624-4AD1-A933-0422B5292AF8} = {60618CAC-2995-4DF9-9914-45C6FC02C995} {7D05F64D-CE5A-42AA-A2C1-E91458F061CF} = {8D53D749-D51C-46F8-A162-9371AAA6C2E7} + {952B513F-8A00-4D74-9271-925AFB3C6252} = {8D53D749-D51C-46F8-A162-9371AAA6C2E7} {2ACDE176-F13F-42FA-8159-C34FA3D37837} = {8D53D749-D51C-46F8-A162-9371AAA6C2E7} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution diff --git a/src/AppInstallerCLI/AppInstallerCLI.vcxproj b/src/AppInstallerCLI/AppInstallerCLI.vcxproj index bca2ba6d58..cdfa718e9e 100644 --- a/src/AppInstallerCLI/AppInstallerCLI.vcxproj +++ b/src/AppInstallerCLI/AppInstallerCLI.vcxproj @@ -71,6 +71,7 @@ + diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj index 4f49b21a6b..d99b7b790f 100644 --- a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj +++ b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj @@ -68,7 +68,10 @@ - + + + + @@ -122,9 +125,9 @@ Disabled _DEBUG;%(PreprocessorDefinitions);CLICOREDLLBUILD - $(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories) - $(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories) - $(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories) + $(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\JsonCppLib;%(AdditionalIncludeDirectories) + $(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\JsonCppLib;%(AdditionalIncludeDirectories) + $(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\JsonCppLib;%(AdditionalIncludeDirectories) true true true @@ -139,7 +142,7 @@ WIN32;%(PreprocessorDefinitions);CLICOREDLLBUILD - $(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories) + $(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\JsonCppLib;%(AdditionalIncludeDirectories) true @@ -152,10 +155,10 @@ true true NDEBUG;%(PreprocessorDefinitions);CLICOREDLLBUILD - $(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories) - $(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories) - $(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories) - $(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;%(AdditionalIncludeDirectories) + $(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\JsonCppLib;%(AdditionalIncludeDirectories) + $(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\JsonCppLib;%(AdditionalIncludeDirectories) + $(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\JsonCppLib;%(AdditionalIncludeDirectories) + $(ProjectDir);$(ProjectDir)..\AppInstallerRepositoryCore;$(ProjectDir)..\AppInstallerRepositoryCore\Public;$(ProjectDir)..\AppInstallerCommonCore\Public;$(ProjectDir)..\JsonCppLib\json;$(ProjectDir)..\JsonCppLib;%(AdditionalIncludeDirectories) true true true diff --git a/src/AppInstallerCLICore/PackageCollection.cpp b/src/AppInstallerCLICore/PackageCollection.cpp index bfcba49b44..072bb05ffd 100644 --- a/src/AppInstallerCLICore/PackageCollection.cpp +++ b/src/AppInstallerCLICore/PackageCollection.cpp @@ -3,7 +3,11 @@ #include "pch.h" #include "PackageCollection.h" + #include "AppInstallerRuntime.h" +#include "winget/JsonSchemaValidation.h" + +#include "PackagesSchema.h" #include #include @@ -106,7 +110,7 @@ namespace AppInstaller::CLI if (!version.empty()) { packageNode[s_PackagesJson_Package_Version] = version; - } + } const std::string& channel = package.VersionAndChannel.GetChannel().ToString(); if (!channel.empty()) @@ -122,7 +126,6 @@ namespace AppInstaller::CLI { Json::Value sourceNode{ Json::ValueType::objectValue }; - Json::Value sourceDetailsNode{ Json::ValueType::objectValue }; sourceDetailsNode[s_PackagesJson_Source_Name] = source.Details.Name; sourceDetailsNode[s_PackagesJson_Source_Argument] = source.Details.Arg; @@ -155,10 +158,38 @@ namespace AppInstaller::CLI return root; } - std::optional TryParseJson(const Json::Value& root) + ParseResult TryParseJson(const Json::Value& root) { - // TODO: Embed schema in binaries & validate file. This will return nullopt on failure. + // Find the schema used for the JSON + if (!(root.isObject() && root.isMember(s_PackagesJson_Schema) && root[s_PackagesJson_Schema].isString())) + { + AICLI_LOG(CLI, Error, << "Import file is missing \"" << s_PackagesJson_Schema << "\" property"); + return ParseResult{ ParseResult::Type::MissingSchema }; + } + + const auto& schemaUri = root[s_PackagesJson_Schema].asString(); + Json::Value schemaJson; + if (schemaUri == s_PackagesJson_SchemaUri_v1_0) + { + schemaJson = JsonSchema::LoadResourceAsSchemaDoc(MAKEINTRESOURCE(IDX_PACKAGES_SCHEMA_V1), MAKEINTRESOURCE(PACKAGESSCHEMA_RESOURCE_TYPE)); + } + else + { + AICLI_LOG(CLI, Error, << "Unrecognized schema for import file: " << schemaUri); + return ParseResult{ ParseResult::Type::UnrecognizedSchema }; + } + + // Validate the JSON against the schema. + valijson::Schema schema; + JsonSchema::PopulateSchema(schemaJson, schema); + + valijson::ValidationResults results; + if (!JsonSchema::Validate(schema, root, results)) + { + return ParseResult{ ParseResult::Type::SchemaValidationFailed, JsonSchema::GetErrorStringFromResults(results) }; + } + // Extract the data from the JSON. PackageCollection packages; packages.ClientVersion = root[s_PackagesJson_WinGetVersion].asString(); for (const auto& sourceNode : root[s_PackagesJson_Sources]) @@ -175,7 +206,7 @@ namespace AppInstaller::CLI } } - return packages; + return ParseResult{ std::move(packages) }; } } } \ No newline at end of file diff --git a/src/AppInstallerCLICore/PackageCollection.h b/src/AppInstallerCLICore/PackageCollection.h index 29b3d6d3e4..3dd53e7161 100644 --- a/src/AppInstallerCLICore/PackageCollection.h +++ b/src/AppInstallerCLICore/PackageCollection.h @@ -53,9 +53,29 @@ namespace AppInstaller::CLI namespace PackagesJson { + struct ParseResult + { + enum class Type + { + MissingSchema, + UnrecognizedSchema, + SchemaValidationFailed, + Success, + }; + + ParseResult(Type result) : Result(result) {} + ParseResult(Type result, std::string_view errors) : Result(result), Errors(errors) {} + ParseResult(PackageCollection&& packages) : Result(Type::Success), Packages(std::move(packages)) {} + + Type Result; + PackageCollection Packages; + std::string Errors; + }; + // Converts a collection of packages to its JSON representation for exporting. Json::Value CreateJson(const PackageCollection& packages); - std::optional TryParseJson(const Json::Value& root); + // Tries to parse a JSON into a collection of packages. + ParseResult TryParseJson(const Json::Value& root); } } \ No newline at end of file diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h index a77af3c85f..7e63e21377 100644 --- a/src/AppInstallerCLICore/Resources.h +++ b/src/AppInstallerCLICore/Resources.h @@ -75,6 +75,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(ImportCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(ImportCommandShortDescription); WINGET_DEFINE_RESOURCE_STRINGID(ImportFileArgumentDescription); + WINGET_DEFINE_RESOURCE_STRINGID(ImportFileHasInvalidSchema); WINGET_DEFINE_RESOURCE_STRINGID(ImportIgnorePackageVersionsArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(ImportIgnoreUnavailableArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(ImportInstallFailed); diff --git a/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp b/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp index 2869c42599..bf04ded8e2 100644 --- a/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp @@ -169,14 +169,25 @@ namespace AppInstaller::CLI::Workflow AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_JSON_INVALID_FILE); } - auto packages = PackagesJson::TryParseJson(jsonRoot); - if (!packages.has_value()) + PackagesJson::ParseResult parseResult = PackagesJson::TryParseJson(jsonRoot); + if (parseResult.Result != PackagesJson::ParseResult::Type::Success) { context.Reporter.Error() << Resource::String::InvalidJsonFile << std::endl; + if (parseResult.Result == PackagesJson::ParseResult::Type::MissingSchema || + parseResult.Result == PackagesJson::ParseResult::Type::UnrecognizedSchema) + { + context.Reporter.Error() << Resource::String::ImportFileHasInvalidSchema << std::endl; + } + else if (parseResult.Result == PackagesJson::ParseResult::Type::SchemaValidationFailed) + { + context.Reporter.Error() << parseResult.Errors << std::endl; + } + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_JSON_INVALID_FILE); } - if (packages->Sources.empty()) + PackageCollection& packages = parseResult.Packages; + if (packages.Sources.empty()) { AICLI_LOG(CLI, Warning, << "No packages to install"); context.Reporter.Info() << Resource::String::NoPackagesFoundInImportFile << std::endl; @@ -186,7 +197,7 @@ namespace AppInstaller::CLI::Workflow if (context.Args.Contains(Execution::Args::Type::IgnoreVersions)) { // Strip out all the version information as we don't need it. - for (auto& source : packages->Sources) + for (auto& source : packages.Sources) { for (auto& package : source.Packages) { @@ -195,7 +206,7 @@ namespace AppInstaller::CLI::Workflow } } - context.Add(packages.value()); + context.Add(std::move(packages)); } void OpenSourcesForImport(Execution::Context& context) diff --git a/src/AppInstallerCLICore/pch.h b/src/AppInstallerCLICore/pch.h index ef55d0a8c8..e62d38f92a 100644 --- a/src/AppInstallerCLICore/pch.h +++ b/src/AppInstallerCLICore/pch.h @@ -7,6 +7,14 @@ #include #include +#pragma warning( push ) +#pragma warning ( disable : 4458 4100 4702 ) +#include +#include +#include +#include +#pragma warning( pop ) + #include #include #include diff --git a/src/AppInstallerCLIE2ETests/ImportCommand.cs b/src/AppInstallerCLIE2ETests/ImportCommand.cs index 912c7cc083..15e724140f 100644 --- a/src/AppInstallerCLIE2ETests/ImportCommand.cs +++ b/src/AppInstallerCLIE2ETests/ImportCommand.cs @@ -32,7 +32,7 @@ public void ImportSuccessful() } // Ignore while we don't have schema validation - // [Test] + [Test] public void ImportInvalidFile() { // Verify failure when trying to import with an invalid file diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw index 8f27b2687d..933e2d9097 100644 --- a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw +++ b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -814,4 +814,7 @@ They can be configured through the settings file 'winget settings'. Path does not exist: + + The JSON file does not specify a recognized schema. + \ No newline at end of file diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj index f14a5bd015..a32b767806 100644 --- a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj +++ b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -55,6 +55,7 @@ + diff --git a/src/AppInstallerCLITests/PackageCollection.cpp b/src/AppInstallerCLITests/PackageCollection.cpp index ce549d562e..32614bfd3a 100644 --- a/src/AppInstallerCLITests/PackageCollection.cpp +++ b/src/AppInstallerCLITests/PackageCollection.cpp @@ -195,8 +195,9 @@ TEST_CASE("PackageCollection_Read_SingleSource", "[PackageCollection]") "WinGetVersion": "1.0.0" })"); - auto parsed = PackagesJson::TryParseJson(json); - REQUIRE(parsed.has_value()); + auto parseResult = PackagesJson::TryParseJson(json); + REQUIRE(parseResult.Result == PackagesJson::ParseResult::Type::Success); + REQUIRE(parseResult.Errors.empty()); PackageCollection::Source source; source.Details.Name = "TestSource"; @@ -213,7 +214,7 @@ TEST_CASE("PackageCollection_Read_SingleSource", "[PackageCollection]") std::vector{ source } }; - ValidateEqualCollections(parsed.value(), expected); + ValidateEqualCollections(parseResult.Packages, expected); } TEST_CASE("PackageCollection_Read_MultipleSources", "[PackageCollection]") @@ -254,8 +255,10 @@ TEST_CASE("PackageCollection_Read_MultipleSources", "[PackageCollection]") ] })"); - auto parsed = PackagesJson::TryParseJson(json); - REQUIRE(parsed.has_value()); + + auto parseResult = PackagesJson::TryParseJson(json); + REQUIRE(parseResult.Result == PackagesJson::ParseResult::Type::Success); + REQUIRE(parseResult.Errors.empty()); PackageCollection::Source source1; source1.Details.Name = "First"; @@ -277,7 +280,7 @@ TEST_CASE("PackageCollection_Read_MultipleSources", "[PackageCollection]") std::vector{ source1, source2 } }; - ValidateEqualCollections(parsed.value(), expected); + ValidateEqualCollections(parseResult.Packages, expected); } TEST_CASE("PackageCollection_Read_RepeatedSource", "[PackageCollection]") @@ -332,8 +335,10 @@ TEST_CASE("PackageCollection_Read_RepeatedSource", "[PackageCollection]") ] })"); - auto parsed = PackagesJson::TryParseJson(json); - REQUIRE(parsed.has_value()); + + auto parseResult = PackagesJson::TryParseJson(json); + REQUIRE(parseResult.Result == PackagesJson::ParseResult::Type::Success); + REQUIRE(parseResult.Errors.empty()); PackageCollection::Source source1; source1.Details.Name = "First"; @@ -356,5 +361,96 @@ TEST_CASE("PackageCollection_Read_RepeatedSource", "[PackageCollection]") std::vector{ source1, source2 } }; - ValidateEqualCollections(parsed.value(), expected); + ValidateEqualCollections(parseResult.Packages, expected); +} + +TEST_CASE("PackageCollection_Read_MissingSchema", "[PackageCollection]") +{ + auto json = ParseJsonString(R"( + { + "CreationDate": "2021-01-01T12:00:00.000", + "Sources": [ + { + "Packages": [ + { + "Id": "test.test" + } + ], + "SourceDetails": { + "Argument": "https://aka.ms/winget", + "Identifier": "TestSourceId", + "Name": "TestSource", + "Type": "Microsoft.PreIndexed.Package" + } + } + ], + "WinGetVersion": "1.0.0" + })"); + + auto parseResult = PackagesJson::TryParseJson(json); + REQUIRE(parseResult.Result == PackagesJson::ParseResult::Type::MissingSchema); + + json = ParseJsonString("\"Not even a JSON object\""); + + parseResult = PackagesJson::TryParseJson(json); + REQUIRE(parseResult.Result == PackagesJson::ParseResult::Type::MissingSchema); +} + +TEST_CASE("PackageCollection_Read_WrongSchema", "[PackageCollection]") +{ + auto json = ParseJsonString(R"( + { + "$schema": "https://aka.ms/winget-settings.schema.json", + "CreationDate": "2021-01-01T12:00:00.000", + "Sources": [ + { + "Packages": [ + { + "Id": "test.test" + } + ], + "SourceDetails": { + "Argument": "https://aka.ms/winget", + "Identifier": "TestSourceId", + "Name": "TestSource", + "Type": "Microsoft.PreIndexed.Package" + } + } + ], + "WinGetVersion": "1.0.0" + })"); + + auto parseResult = PackagesJson::TryParseJson(json); + REQUIRE(parseResult.Result == PackagesJson::ParseResult::Type::UnrecognizedSchema); +} + +TEST_CASE("PackageCollection_Read_SchemaValidationFail", "[PackageCollection]") +{ + auto json = ParseJsonString(R"( + { + "$schema": "https://aka.ms/winget-packages.schema.1.0.json", + "CreationDate": "2021-01-01T12:00:00.000", + "NotSources": [ + { + "Packages": [ + { + "Id": "test.test" + } + ], + "SourceDetails": { + "Argument": "https://aka.ms/winget", + "Identifier": "TestSourceId", + "Name": "TestSource", + "Type": "Microsoft.PreIndexed.Package" + } + } + ], + "WinGetVersion": "1.0.0" + })"); + + auto parseResult = PackagesJson::TryParseJson(json); + INFO(parseResult.Errors); + + REQUIRE(parseResult.Result == PackagesJson::ParseResult::Type::SchemaValidationFailed); + REQUIRE(parseResult.Errors.find("Missing required property 'Sources'.") != std::string::npos); } \ No newline at end of file diff --git a/src/AppInstallerCLITests/WorkFlow.cpp b/src/AppInstallerCLITests/WorkFlow.cpp index 47711e7bc0..81863b7bfc 100644 --- a/src/AppInstallerCLITests/WorkFlow.cpp +++ b/src/AppInstallerCLITests/WorkFlow.cpp @@ -1313,14 +1313,11 @@ TEST_CASE("ImportFlow_InvalidJsonFile", "[ImportFlow][workflow]") context.Args.AddArg(Execution::Args::Type::ImportFile, TestDataFile("ImportFile-Bad-Invalid.json").GetPath().string()); ImportCommand importCommand({}); - // TODO: Enable when we have schema validation - /* importCommand.Execute(context); INFO(importOutput.str()); // Command should have failed REQUIRE_TERMINATED_WITH(context, APPINSTALLER_CLI_ERROR_JSON_INVALID_FILE); - */ } void VerifyMotw(const std::filesystem::path& testFile, DWORD zone) diff --git a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj index 62360f8bd9..c298f0310e 100644 --- a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj +++ b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj @@ -267,6 +267,7 @@ + @@ -310,6 +311,7 @@ true + diff --git a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters index b547e5f2d9..c3ebb155ce 100644 --- a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters +++ b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters @@ -156,6 +156,9 @@ Public\winget + + Public\winget + @@ -263,6 +266,9 @@ Manifest + + Source Files + diff --git a/src/AppInstallerCommonCore/JsonSchemaValidation.cpp b/src/AppInstallerCommonCore/JsonSchemaValidation.cpp new file mode 100644 index 0000000000..f0a57ac7a6 --- /dev/null +++ b/src/AppInstallerCommonCore/JsonSchemaValidation.cpp @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "winget/JsonSchemaValidation.h" + +namespace AppInstaller::JsonSchema +{ + std::string LoadResourceAsString(PCWSTR resourceName, PCWSTR resourceType) + { + HMODULE resourceModule = NULL; + GetModuleHandleEx( + GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, + (PCWSTR)LoadResourceAsString, + &resourceModule); + THROW_LAST_ERROR_IF_NULL(resourceModule); + + HRSRC resourceInfoHandle = FindResource(resourceModule, resourceName, resourceType); + THROW_LAST_ERROR_IF_NULL(resourceInfoHandle); + + HGLOBAL resourceMemoryHandle = LoadResource(resourceModule, resourceInfoHandle); + THROW_LAST_ERROR_IF_NULL(resourceMemoryHandle); + + ULONG resourceSize = 0; + char* resourceContent = NULL; + resourceSize = SizeofResource(resourceModule, resourceInfoHandle); + THROW_LAST_ERROR_IF(resourceSize == 0); + + resourceContent = reinterpret_cast(LockResource(resourceMemoryHandle)); + THROW_HR_IF_NULL(E_UNEXPECTED, resourceContent); + + std::string resourceStr; + resourceStr.assign(resourceContent, resourceSize); + + return resourceStr; + } + + Json::Value LoadSchemaDoc(const std::string& schemaStr) + { + Json::Value schemaJson; + int schemaLength = static_cast(schemaStr.length()); + Json::CharReaderBuilder charReaderBuilder; + const std::unique_ptr jsonReader(charReaderBuilder.newCharReader()); + std::string errorMsg; + if (!jsonReader->parse(schemaStr.c_str(), schemaStr.c_str() + schemaLength, &schemaJson, &errorMsg)) { + THROW_HR_MSG(E_UNEXPECTED, "Jsoncpp parser failed to parse the schema doc. Reason: %s", errorMsg.c_str()); + } + + return schemaJson; + } + + Json::Value LoadResourceAsSchemaDoc(PCWSTR resourceName, PCWSTR resourceType) + { + return LoadSchemaDoc(LoadResourceAsString(resourceName, resourceType)); + } + + void PopulateSchema(const Json::Value& schemaJson, valijson::Schema& schema) + { + valijson::SchemaParser schemaParser; + valijson::adapters::JsonCppAdapter jsonSchemaAdapter(schemaJson); + schemaParser.populateSchema(jsonSchemaAdapter, schema); + } + + bool Validate(const valijson::Schema& schema, const Json::Value& json, valijson::ValidationResults& results) + { + valijson::Validator schemaValidator; + valijson::adapters::JsonCppAdapter jsonAdapter(json); + return schemaValidator.validate(schema, jsonAdapter, &results); + } + + std::string GetErrorStringFromResults(valijson::ValidationResults& results) + { + valijson::ValidationResults::Error error; + std::stringstream ss; + + ss << "Schema validation failed." << std::endl; + while (results.popError(error)) + { + std::string context; + for (auto itr = error.context.begin(); itr != error.context.end(); itr++) + { + context += *itr; + } + + ss << "Error context: " << context << " Description: " << error.description << std::endl; + } + + return ss.str(); + } +} \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Manifest/ManifestSchemaValidation.cpp b/src/AppInstallerCommonCore/Manifest/ManifestSchemaValidation.cpp index 3c272369ab..80c7c05d6f 100644 --- a/src/AppInstallerCommonCore/Manifest/ManifestSchemaValidation.cpp +++ b/src/AppInstallerCommonCore/Manifest/ManifestSchemaValidation.cpp @@ -2,6 +2,7 @@ // Licensed under the MIT License. #include "pch.h" #include "winget/Yaml.h" +#include "winget/JsonSchemaValidation.h" #include "winget/ManifestCommon.h" #include "winget/ManifestSchemaValidation.h" #include "winget/ManifestYamlParser.h" @@ -86,35 +87,6 @@ namespace AppInstaller::Manifest::YamlParser } } - std::string LoadResourceAsString(PCWSTR resourceName, PCWSTR resourceType) - { - HMODULE resourceModule = NULL; - GetModuleHandleEx( - GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, - (PCWSTR)LoadResourceAsString, - &resourceModule); - THROW_LAST_ERROR_IF_NULL(resourceModule); - - HRSRC resourceInfoHandle = FindResource(resourceModule, resourceName, resourceType); - THROW_LAST_ERROR_IF_NULL(resourceInfoHandle); - - HGLOBAL resourceMemoryHandle = LoadResource(resourceModule, resourceInfoHandle); - THROW_LAST_ERROR_IF_NULL(resourceMemoryHandle); - - ULONG resourceSize = 0; - char* resourceContent = NULL; - resourceSize = SizeofResource(resourceModule, resourceInfoHandle); - THROW_LAST_ERROR_IF(resourceSize == 0); - - resourceContent = reinterpret_cast(LockResource(resourceMemoryHandle)); - THROW_HR_IF_NULL(E_UNEXPECTED, resourceContent); - - std::string resourceStr; - resourceStr.assign(resourceContent, resourceSize); - - return resourceStr; - } - Json::Value LoadSchemaDoc(const ManifestVer& manifestVersion, ManifestTypeEnum manifestType) { std::string schemaStr; @@ -124,19 +96,19 @@ namespace AppInstaller::Manifest::YamlParser switch (manifestType) { case AppInstaller::Manifest::ManifestTypeEnum::Singleton: - schemaStr = LoadResourceAsString(MAKEINTRESOURCE(IDX_MANIFEST_SCHEMA_V1_SINGLETON), MAKEINTRESOURCE(MANIFESTSCHEMA_RESOURCE_TYPE)); + schemaStr = JsonSchema::LoadResourceAsString(MAKEINTRESOURCE(IDX_MANIFEST_SCHEMA_V1_SINGLETON), MAKEINTRESOURCE(MANIFESTSCHEMA_RESOURCE_TYPE)); break; case AppInstaller::Manifest::ManifestTypeEnum::Version: - schemaStr = LoadResourceAsString(MAKEINTRESOURCE(IDX_MANIFEST_SCHEMA_V1_VERSION), MAKEINTRESOURCE(MANIFESTSCHEMA_RESOURCE_TYPE)); + schemaStr = JsonSchema::LoadResourceAsString(MAKEINTRESOURCE(IDX_MANIFEST_SCHEMA_V1_VERSION), MAKEINTRESOURCE(MANIFESTSCHEMA_RESOURCE_TYPE)); break; case AppInstaller::Manifest::ManifestTypeEnum::Installer: - schemaStr = LoadResourceAsString(MAKEINTRESOURCE(IDX_MANIFEST_SCHEMA_V1_INSTALLER), MAKEINTRESOURCE(MANIFESTSCHEMA_RESOURCE_TYPE)); + schemaStr = JsonSchema::LoadResourceAsString(MAKEINTRESOURCE(IDX_MANIFEST_SCHEMA_V1_INSTALLER), MAKEINTRESOURCE(MANIFESTSCHEMA_RESOURCE_TYPE)); break; case AppInstaller::Manifest::ManifestTypeEnum::DefaultLocale: - schemaStr = LoadResourceAsString(MAKEINTRESOURCE(IDX_MANIFEST_SCHEMA_V1_DEFAULTLOCALE), MAKEINTRESOURCE(MANIFESTSCHEMA_RESOURCE_TYPE)); + schemaStr = JsonSchema::LoadResourceAsString(MAKEINTRESOURCE(IDX_MANIFEST_SCHEMA_V1_DEFAULTLOCALE), MAKEINTRESOURCE(MANIFESTSCHEMA_RESOURCE_TYPE)); break; case AppInstaller::Manifest::ManifestTypeEnum::Locale: - schemaStr = LoadResourceAsString(MAKEINTRESOURCE(IDX_MANIFEST_SCHEMA_V1_LOCALE), MAKEINTRESOURCE(MANIFESTSCHEMA_RESOURCE_TYPE)); + schemaStr = JsonSchema::LoadResourceAsString(MAKEINTRESOURCE(IDX_MANIFEST_SCHEMA_V1_LOCALE), MAKEINTRESOURCE(MANIFESTSCHEMA_RESOURCE_TYPE)); break; default: THROW_HR(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED)); @@ -144,19 +116,10 @@ namespace AppInstaller::Manifest::YamlParser } else { - schemaStr = LoadResourceAsString(MAKEINTRESOURCE(IDX_MANIFEST_SCHEMA_PREVIEW), MAKEINTRESOURCE(MANIFESTSCHEMA_RESOURCE_TYPE)); + schemaStr = JsonSchema::LoadResourceAsString(MAKEINTRESOURCE(IDX_MANIFEST_SCHEMA_PREVIEW), MAKEINTRESOURCE(MANIFESTSCHEMA_RESOURCE_TYPE)); } - Json::Value schemaJson; - int schemaLength = static_cast(schemaStr.length()); - Json::CharReaderBuilder charReaderBuilder; - const std::unique_ptr jsonReader(charReaderBuilder.newCharReader()); - std::string errorMsg; - if (!jsonReader->parse(schemaStr.c_str(), schemaStr.c_str() + schemaLength, &schemaJson, &errorMsg)) { - THROW_HR_MSG(E_UNEXPECTED, "Jsoncpp parser failed to parse the schema doc. Reason: %s", errorMsg.c_str()); - } - - return schemaJson; + return JsonSchema::LoadSchemaDoc(schemaStr); } std::vector ValidateAgainstSchema(const std::vector& manifestList, const ManifestVer& manifestVersion) @@ -164,7 +127,6 @@ namespace AppInstaller::Manifest::YamlParser std::vector errors; // A list of schema validator to avoid multiple loadings of same schema std::map schemaList; - valijson::Validator schemaValidator; for (const auto& entry : manifestList) { @@ -173,36 +135,17 @@ namespace AppInstaller::Manifest::YamlParser // Copy constructor of valijson::Schema was private valijson::Schema& newSchema = schemaList.emplace( std::piecewise_construct, std::make_tuple(entry.ManifestType), std::make_tuple()).first->second; - valijson::SchemaParser schemaParser; Json::Value schemaJson = LoadSchemaDoc(manifestVersion, entry.ManifestType); - valijson::adapters::JsonCppAdapter jsonSchemaAdapter(schemaJson); - schemaParser.populateSchema(jsonSchemaAdapter, newSchema); + JsonSchema::PopulateSchema(schemaJson, newSchema); } const auto& schema = schemaList.find(entry.ManifestType)->second; - Json::Value manifestJson = ManifestYamlNodeToJson(entry.Root); - valijson::adapters::JsonCppAdapter manifestJsonAdapter(manifestJson); valijson::ValidationResults results; - if (!schemaValidator.validate(schema, manifestJsonAdapter, &results)) + if (!JsonSchema::Validate(schema, manifestJson, results)) { - valijson::ValidationResults::Error error; - std::stringstream ss; - - ss << "Schema validation failed." << std::endl; - while (results.popError(error)) - { - std::string context; - for (auto itr = error.context.begin(); itr != error.context.end(); itr++) - { - context += *itr; - } - - ss << "Error context: " << context << " Description: " << error.description << std::endl; - } - - errors.emplace_back(ValidationError::MessageWithFile(ss.str(), entry.FileName)); + errors.emplace_back(ValidationError::MessageWithFile(JsonSchema::GetErrorStringFromResults(results), entry.FileName)); } } diff --git a/src/AppInstallerCommonCore/Public/winget/JsonSchemaValidation.h b/src/AppInstallerCommonCore/Public/winget/JsonSchemaValidation.h new file mode 100644 index 0000000000..1c921f8921 --- /dev/null +++ b/src/AppInstallerCommonCore/Public/winget/JsonSchemaValidation.h @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include + +namespace AppInstaller::JsonSchema +{ + // Load an embedded resource from binary and return as std::string + std::string LoadResourceAsString(PCWSTR resourceName, PCWSTR resourceType); + + // Load schema as parsed json doc + Json::Value LoadSchemaDoc(const std::string& schemaStr); + + // Load an embedded resource from binary and return as Json::Value + Json::Value LoadResourceAsSchemaDoc(PCWSTR resourceName, PCWSTR resourceType); + + // Populate a valijson Schema object from a json value + void PopulateSchema(const Json::Value& schemaJson, valijson::Schema& schema); + + // Validate a json doc with a schema + // Returns whether it was successful and fills the results object + bool Validate(const valijson::Schema& schema, const Json::Value& json, valijson::ValidationResults& results); + + // Extracts the error messages from a result into a single non-localized string + std::string GetErrorStringFromResults(valijson::ValidationResults& results); +} \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Public/winget/ManifestSchemaValidation.h b/src/AppInstallerCommonCore/Public/winget/ManifestSchemaValidation.h index 1dc8793d5e..35488bae24 100644 --- a/src/AppInstallerCommonCore/Public/winget/ManifestSchemaValidation.h +++ b/src/AppInstallerCommonCore/Public/winget/ManifestSchemaValidation.h @@ -11,9 +11,6 @@ namespace AppInstaller::Manifest::YamlParser // Forward declarations struct YamlManifestInfo; - // Load an embedded resource from binary and return as std::string - std::string LoadResourceAsString(PCWSTR resourceName, PCWSTR resourceType); - // Load manifest schema as parsed json doc Json::Value LoadSchemaDoc(const ManifestVer& manifestVersion, ManifestTypeEnum manifestType); diff --git a/src/WinGetSchemas/PackagesSchema.h b/src/WinGetSchemas/PackagesSchema.h new file mode 100644 index 0000000000..f9f0f49124 --- /dev/null +++ b/src/WinGetSchemas/PackagesSchema.h @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once + +#define PACKAGESSCHEMA_RESOURCE_TYPE 300 + +#define IDX_PACKAGES_SCHEMA_V1 301 diff --git a/src/WinGetSchemas/WinGetSchemas.rc b/src/WinGetSchemas/WinGetSchemas.rc new file mode 100644 index 0000000000..dea581d7cc --- /dev/null +++ b/src/WinGetSchemas/WinGetSchemas.rc @@ -0,0 +1,66 @@ +// Microsoft Visual C++ generated resource script. +// +#include "resource.h" +#include "PackagesSchema.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE 9, 1 + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED + +///////////////////////////////////////////////////////////////////////////// +// +// Packages schema +// +IDX_PACKAGES_SCHEMA_V1 PACKAGESSCHEMA_RESOURCE_TYPE "..\\..\\schemas\\JSON\\packages\\packages.schema.1.0.json" diff --git a/src/WinGetSchemas/WinGetSchemas.vcxitems b/src/WinGetSchemas/WinGetSchemas.vcxitems new file mode 100644 index 0000000000..f171001df3 --- /dev/null +++ b/src/WinGetSchemas/WinGetSchemas.vcxitems @@ -0,0 +1,27 @@ + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + true + {952b513f-8a00-4d74-9271-925afb3c6252} + + + + %(AdditionalIncludeDirectories);$(MSBuildThisFileDirectory) + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/WinGetSchemas/WinGetSchemas.vcxitems.filters b/src/WinGetSchemas/WinGetSchemas.vcxitems.filters new file mode 100644 index 0000000000..08227f67a3 --- /dev/null +++ b/src/WinGetSchemas/WinGetSchemas.vcxitems.filters @@ -0,0 +1,26 @@ + + + + + {9b8a4c46-6227-45fe-840b-8f50fb10ddb1} + + + {931a4cce-b01f-4d2f-b39a-8600f2010a97} + + + + + settings + + + packages + + + + + + + + + + \ No newline at end of file diff --git a/src/WinGetSchemas/resource.h b/src/WinGetSchemas/resource.h new file mode 100644 index 0000000000..80bec7af27 --- /dev/null +++ b/src/WinGetSchemas/resource.h @@ -0,0 +1,14 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by WinGetSchemas.rc + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 101 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif