diff --git a/libmodulemd.spec.in b/libmodulemd.spec.in index 42a9ec941..9ae774514 100644 --- a/libmodulemd.spec.in +++ b/libmodulemd.spec.in @@ -128,6 +128,7 @@ ln -s libmodulemd.so.%{libmodulemd_v1_version} \ %files %license COPYING %doc README.md +%{_bindir}/modulemd-validator %{_libdir}/%{name}.so.2* %dir %{_libdir}/girepository-1.0 %{_libdir}/girepository-1.0/Modulemd-2.0.typelib @@ -154,7 +155,7 @@ ln -s libmodulemd.so.%{libmodulemd_v1_version} \ %files -n libmodulemd1 %license COPYING %doc README.md -%{_bindir}/modulemd-validator +%{_bindir}/modulemd-validator-v1 %{_libdir}/%{name}.so.1* %dir %{_libdir}/girepository-1.0 %{_libdir}/girepository-1.0/Modulemd-1.0.typelib diff --git a/modulemd/meson.build b/modulemd/meson.build index fe50d9815..cdf006330 100644 --- a/modulemd/meson.build +++ b/modulemd/meson.build @@ -140,6 +140,10 @@ modulemd_v2_srcs = files( 'v2/modulemd-yaml-util.c', ) +modulemd_v2_validator_srcs = files ( + 'v2/modulemd-validator.c', +) + modulemd_v2_hdrs = files( 'v2/include/modulemd-2.0/modulemd.h', 'v2/include/modulemd-2.0/modulemd-buildopts.h', @@ -239,6 +243,7 @@ test('clang_format', clang_format, modulemd_v2_srcs + modulemd_v2_hdrs + modulemd_v2_priv_hdrs + + modulemd_v2_validator_srcs + test_v2_srcs + test_v2_priv_hdrs) diff --git a/modulemd/v1/meson.build b/modulemd/v1/meson.build index ad65dbd0d..929c6af07 100644 --- a/modulemd/v1/meson.build +++ b/modulemd/v1/meson.build @@ -55,8 +55,8 @@ else endif -modulemd_validator = executable( - 'modulemd-validator', +modulemd_validator_v1 = executable( + 'modulemd-validator-v1', 'modulemd-validator.c', dependencies : [ modulemd_v1_dep, diff --git a/modulemd/v2/include/modulemd-2.0/private/modulemd-module-index-private.h b/modulemd/v2/include/modulemd-2.0/private/modulemd-module-index-private.h index 65b0c22aa..fd37ab30d 100644 --- a/modulemd/v2/include/modulemd-2.0/private/modulemd-module-index-private.h +++ b/modulemd/v2/include/modulemd-2.0/private/modulemd-module-index-private.h @@ -14,6 +14,7 @@ #pragma once #include +#include #include "modulemd-module-index.h" G_BEGIN_DECLS @@ -27,6 +28,40 @@ G_BEGIN_DECLS */ +/** + * modulemd_module_index_update_from_parser: + * @self: (in): This #ModulemdModuleIndex object + * @parser: (inout): An initialized YAML parser that has not yet processed any + * events. + * @strict: (in): Whether the parser should return failure if it encounters an + * unknown mapping key or if it should ignore it. + * @autogen_module_name: (in): When parsing a module stream that contains no + * module name or stream name, whether to autogenerate one or not. This option + * should be used only for validation tools such as modulemd-validator. Normal + * public routines should always set this to FALSE. + * @failures: (out) (element-type ModulemdSubdocumentInfo) (transfer container): + * An array containing any subdocuments from the YAML file that failed to parse. + * See #ModulemdSubdocumentInfo for more details. If the array is NULL, it will + * be allocated by this function. If it is non-NULL, this function will append + * to it. + * @error: (out): A GError containing additional information if this function + * fails in a way that prevents program continuation. + * + * Returns: TRUE if the update was successful. Returns FALSE and sets failures + * approriately if any of the YAML subdocuments were invalid or sets @error if + * there was a fatal parse error. + * + * Since: 2.0 + */ +gboolean +modulemd_module_index_update_from_parser (ModulemdModuleIndex *self, + yaml_parser_t *parser, + gboolean strict, + gboolean autogen_module_name, + GPtrArray **failures, + GError **error); + + /** * modulemd_module_index_merge: * @from: (in) (transfer none): The #ModulemdModuleIndex whose contents are diff --git a/modulemd/v2/meson.build b/modulemd/v2/meson.build index 79d1583f6..ab5b7aa57 100644 --- a/modulemd/v2/meson.build +++ b/modulemd/v2/meson.build @@ -66,6 +66,18 @@ else ) endif +modulemd_v2_validator = executable( + 'modulemd-validator', + sources : modulemd_v2_validator_srcs, + include_directories : v2_include_dirs, + dependencies : [ + gobject, + yaml, + modulemd_v2_dep + ], + install : true +) + v2_header_path = 'modulemd-2.0' install_headers( diff --git a/modulemd/v2/modulemd-module-index.c b/modulemd/v2/modulemd-module-index.c index 731b36769..741ad42de 100644 --- a/modulemd/v2/modulemd-module-index.c +++ b/modulemd/v2/modulemd-module-index.c @@ -23,6 +23,7 @@ #include "private/modulemd-defaults-v1-private.h" #include "private/modulemd-subdocument-info-private.h" #include "private/modulemd-module-index-private.h" +#include "private/modulemd-module-stream-private.h" #include "private/modulemd-module-stream-v1-private.h" #include "private/modulemd-module-stream-v2-private.h" #include "private/modulemd-translation-private.h" @@ -121,11 +122,13 @@ static gboolean add_subdoc (ModulemdModuleIndex *self, ModulemdSubdocumentInfo *subdoc, gboolean strict, + gboolean autogen_module_name, GError **error) { g_autoptr (ModulemdModuleStream) stream = NULL; g_autoptr (ModulemdTranslation) translation = NULL; g_autoptr (ModulemdDefaults) defaults = NULL; + g_autofree gchar *name = NULL; switch (modulemd_subdocument_info_get_doctype (subdoc)) { @@ -135,20 +138,12 @@ add_subdoc (ModulemdModuleIndex *self, case MD_MODULESTREAM_VERSION_ONE: stream = MODULEMD_MODULE_STREAM ( modulemd_module_stream_v1_parse_yaml (subdoc, strict, error)); - if (stream == NULL) - return FALSE; - if (!modulemd_module_index_add_module_stream (self, stream, error)) - return FALSE; break; case MD_MODULESTREAM_VERSION_TWO: stream = (ModulemdModuleStream *)modulemd_module_stream_v2_parse_yaml ( subdoc, strict, error); - if (stream == NULL) - return FALSE; - if (!modulemd_module_index_add_module_stream (self, stream, error)) - return FALSE; break; default: @@ -158,6 +153,32 @@ add_subdoc (ModulemdModuleIndex *self, "Invalid mdversion for a stream object"); return FALSE; } + + if (stream == NULL) + return FALSE; + + if (autogen_module_name && + !modulemd_module_stream_get_module_name (stream)) + { + name = g_strdup_printf ("__unnamed_module_%d", + g_hash_table_size (self->modules) + 1); + modulemd_module_stream_set_module_name (stream, name); + g_clear_pointer (&name, g_free); + } + + if (autogen_module_name && + !modulemd_module_stream_get_stream_name (stream)) + { + name = g_strdup_printf ("__unnamed_stream_%d", + g_hash_table_size (self->modules) + 1); + modulemd_module_stream_set_stream_name (stream, name); + g_clear_pointer (&name, g_free); + } + + + if (!modulemd_module_index_add_module_stream (self, stream, error)) + return FALSE; + break; case MODULEMD_YAML_DOC_DEFAULTS: @@ -201,10 +222,11 @@ add_subdoc (ModulemdModuleIndex *self, } -static gboolean +gboolean modulemd_module_index_update_from_parser (ModulemdModuleIndex *self, yaml_parser_t *parser, gboolean strict, + gboolean autogen_module_name, GPtrArray **failures, GError **error) { @@ -213,6 +235,9 @@ modulemd_module_index_update_from_parser (ModulemdModuleIndex *self, g_autoptr (ModulemdSubdocumentInfo) subdoc = NULL; MMD_INIT_YAML_EVENT (event); + if (*failures == NULL) + *failures = g_ptr_array_new_with_free_func (g_object_unref); + YAML_PARSER_PARSE_WITH_EXIT_BOOL (parser, &event, error); if (event.type != YAML_STREAM_START_EVENT) MMD_YAML_ERROR_EVENT_EXIT_BOOL ( @@ -236,7 +261,8 @@ modulemd_module_index_update_from_parser (ModulemdModuleIndex *self, else { /* Initial parsing worked, parse further */ - if (!add_subdoc (self, subdoc, strict, error)) + if (!add_subdoc ( + self, subdoc, strict, autogen_module_name, error)) { modulemd_subdocument_info_set_gerror (subdoc, *error); g_clear_pointer (error, g_error_free); @@ -244,8 +270,8 @@ modulemd_module_index_update_from_parser (ModulemdModuleIndex *self, g_ptr_array_add (*failures, g_steal_pointer (&subdoc)); all_passed = FALSE; } - g_clear_pointer (&subdoc, g_object_unref); } + g_clear_pointer (&subdoc, g_object_unref); break; case YAML_STREAM_END_EVENT: done = TRUE; break; @@ -479,7 +505,7 @@ modulemd_module_index_update_from_string (ModulemdModuleIndex *self, &parser, (const unsigned char *)yaml_string, strlen (yaml_string)); return modulemd_module_index_update_from_parser ( - self, &parser, strict, failures, error); + self, &parser, strict, FALSE, failures, error); } @@ -507,7 +533,7 @@ modulemd_module_index_update_from_stream (ModulemdModuleIndex *self, yaml_parser_set_input_file (&parser, yaml_stream); return modulemd_module_index_update_from_parser ( - self, &parser, strict, failures, error); + self, &parser, strict, FALSE, failures, error); } diff --git a/modulemd/v2/modulemd-validator.c b/modulemd/v2/modulemd-validator.c new file mode 100644 index 000000000..7ba52f773 --- /dev/null +++ b/modulemd/v2/modulemd-validator.c @@ -0,0 +1,213 @@ +/* + * This file is part of libmodulemd + * Copyright 2018 Stephen Gallagher + * + * Fedora-License-Identifier: MIT + * SPDX-2.0-License-Identifier: MIT + * SPDX-3.0-License-Identifier: MIT + * + * This program is free software. + * For more information on the license, see COPYING. + * For more information on free software, see . + */ + + +#include "modulemd.h" +#include "private/modulemd-module-index-private.h" +#include "private/modulemd-yaml.h" + +#include +#include +#include + +enum mmd_verbosity +{ + MMD_QUIET = -1, + MMD_DEFAULT, + MMD_VERBOSE, + MMD_DEBUG +}; + +struct validator_options +{ + enum mmd_verbosity verbosity; + gchar **filenames; +}; + +struct validator_options options = { 0, NULL }; + +static gboolean +set_verbosity (const gchar *option_name, + const gchar *value, + gpointer data, + GError **error) +{ + g_autofree gchar *debugging_env = NULL; + if (g_strcmp0 ("-v", option_name) == 0 || + g_strcmp0 ("--verbose", option_name) == 0) + { + if (options.verbosity < MMD_VERBOSE) + { + options.verbosity = MMD_VERBOSE; + } + } + else if (g_strcmp0 ("--debug", option_name) == 0) + { + if (options.verbosity < MMD_DEBUG) + { + options.verbosity = MMD_DEBUG; + const gchar *old_debug = g_getenv ("G_MESSAGES_DEBUG"); + if (old_debug != NULL) + { + debugging_env = + g_strdup_printf ("%s,%s", old_debug, G_LOG_DOMAIN); + } + else + { + debugging_env = g_strdup (G_LOG_DOMAIN); + } + g_setenv ("G_MESSAGES_DEBUG", debugging_env, TRUE); + } + } + else if (g_strcmp0 ("-q", option_name) == 0 || + g_strcmp0 ("--quiet", option_name) == 0) + { + options.verbosity = MMD_QUIET; + } + else + { + /* We shouldn't be called under any other circumstance */ + g_set_error (error, + G_OPTION_ERROR, + G_OPTION_ERROR_FAILED, + "Called for unknown option \"%s\"", + option_name); + return FALSE; + } + return TRUE; +} + +// clang-format off +static GOptionEntry entries[] = { + { "quiet", 'q', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, set_verbosity, "Print no output", NULL }, + { "verbose", 'v', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, set_verbosity, "Be verbose", NULL }, + { "debug", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, set_verbosity, "Output debugging messages", NULL }, + { G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &options.filenames, "Files to be validated", NULL }, + { NULL } }; +// clang-format on + + +static gboolean +parse_file (const gchar *filename, GPtrArray **failures, GError **error) +{ + MMD_INIT_YAML_PARSER (parser); + MMD_INIT_YAML_EVENT (event); + g_autoptr (FILE) yaml_stream = NULL; + int saved_errno; + g_autoptr (ModulemdModuleIndex) index = NULL; + + if (options.verbosity >= MMD_VERBOSE) + { + g_fprintf (stdout, "Validating %s\n", filename); + } + + /* Parse documents */ + yaml_stream = g_fopen (filename, "rb"); + saved_errno = errno; + + if (yaml_stream == NULL) + { + if (options.verbosity >= MMD_DEFAULT) + { + g_fprintf (stdout, + "Failed to open file %s: %s\n", + filename, + g_strerror (saved_errno)); + } + return FALSE; + } + + + yaml_parser_set_input_file (&parser, yaml_stream); + + index = modulemd_module_index_new (); + return modulemd_module_index_update_from_parser ( + index, &parser, TRUE, TRUE, failures, error); +} + + +int +main (int argc, char *argv[]) +{ + const char *filename; + g_autoptr (GOptionContext) context; + g_autoptr (GError) error = NULL; + gsize num_invalid = 0; + gboolean ret; + g_autoptr (GPtrArray) failures = NULL; + ModulemdSubdocumentInfo *doc = NULL; + + setlocale (LC_ALL, ""); + + context = g_option_context_new ("FILES - Simple modulemd YAML validator"); + g_option_context_add_main_entries (context, entries, "modulemd-validator"); + if (!g_option_context_parse (context, &argc, &argv, &error)) + { + g_print ("option parsing failed: %s\n", error->message); + exit (1); + } + + if (!(options.filenames && options.filenames[0])) + { + g_fprintf (stderr, + "At least one file must be specified on the command-line\n"); + return EXIT_FAILURE; + } + + for (gsize i = 0; options.filenames[i]; i++) + { + filename = options.filenames[i]; + + ret = parse_file (filename, &failures, &error); + if (!ret) + { + num_invalid++; + if (options.verbosity >= MMD_DEFAULT) + { + g_fprintf (stderr, "%s failed to validate\n", filename); + + if (error != NULL) + { + /* Unparseable content */ + g_fprintf (stderr, + "%s could not be read in its entirety: %s\n", + filename, + error->message); + } + if (failures) + { + for (gsize j = 0; j < failures->len; j++) + { + doc = MODULEMD_SUBDOCUMENT_INFO ( + g_ptr_array_index (failures, i)); + g_printf ( + "\nFailed subdocument (%s): \n%s\n", + modulemd_subdocument_info_get_gerror (doc)->message, + modulemd_subdocument_info_get_yaml (doc)); + } + } + } + } + else + { + if (options.verbosity >= MMD_DEFAULT) + { + g_printf ("%s validated successfully\n", filename); + } + } + g_clear_error (&error); + g_clear_pointer (&failures, g_ptr_array_unref); + } + + return num_invalid; +}