diff --git a/compose/asc-compose.c b/compose/asc-compose.c index f14b6f0f..0fd2c6d1 100644 --- a/compose/asc-compose.c +++ b/compose/asc-compose.c @@ -1409,6 +1409,7 @@ asc_compose_process_task_cb (AscComposeTask *ctask, AscCompose *compose) /* process metadata */ for (guint i = 0; i < mi_fnames->len; i++) { g_autoptr(GBytes) mi_bytes = NULL; + g_autoptr(GBytes) rel_bytes = NULL; g_autoptr(GError) local_error = NULL; g_autoptr(AsComponent) cpt = NULL; g_autofree gchar *mi_basename = NULL; @@ -1455,13 +1456,23 @@ asc_compose_process_task_cb (AscComposeTask *ctask, AscCompose *compose) g_strdup (as_component_get_id (cpt))); } + /* process any release information of this component and download release data if needed */ + asc_process_metainfo_releases (ctask->result, + ctask->unit, + cpt, + mi_fname, + as_flags_contains (priv->flags, ASC_COMPOSE_FLAG_ALLOW_NET), + acurl, + &rel_bytes); + /* validate the data */ if (as_flags_contains (priv->flags, ASC_COMPOSE_FLAG_VALIDATE)) { asc_validate_metainfo_data_for_component (ctask->result, - validator, - cpt, - mi_bytes, - mi_basename); + validator, + cpt, + mi_bytes, + mi_basename, + rel_bytes); } /* legacy support: Synthesize launchable entry if none was set, diff --git a/compose/asc-hint-tags.c b/compose/asc-hint-tags.c index dde85729..5f76ff7a 100644 --- a/compose/asc-hint-tags.c +++ b/compose/asc-hint-tags.c @@ -103,6 +103,16 @@ AscHintTagStatic asc_hint_tag_list[] = { "type= property of the component root-node in the MetaInfo XML file does not contain a spelling mistake." }, + { "metainfo-releases-download-failed", + AS_ISSUE_SEVERITY_WARNING, + "Unable to download release information from {{url}}. The error message was: {{msg}}." + }, + + { "metainfo-releases-read-failed", + AS_ISSUE_SEVERITY_ERROR, + "Unable to read release information from {{path}}. The error message was: {{msg}}." + }, + { "file-read-error", AS_ISSUE_SEVERITY_ERROR, "Unable to read data from file {{fname}}: {{msg}}", diff --git a/compose/asc-utils-metainfo.c b/compose/asc-utils-metainfo.c index e10784c4..179f8616 100644 --- a/compose/asc-utils-metainfo.c +++ b/compose/asc-utils-metainfo.c @@ -95,15 +95,6 @@ asc_parse_metainfo_data (AscResult *cres, AsMetadata *mdata, GBytes *bytes, cons return NULL; } - /* limit the amount of releases that we add to the output metadata. - * since releases are sorted with the newest one at the top, we will only - * remove the older ones. */ - if (as_component_get_kind (cpt) != AS_COMPONENT_KIND_OPERATING_SYSTEM) { - GPtrArray *releases = as_component_get_releases (cpt); - if (releases->len > MAX_RELEASE_INFO_COUNT) - g_ptr_array_set_size (releases, MAX_RELEASE_INFO_COUNT); - } - return g_object_ref (cpt); } @@ -132,6 +123,97 @@ asc_parse_metainfo_data_simple (AscResult *cres, GBytes *bytes, const gchar *mi_ mi_basename); } +/** + * asc_process_metainfo_releases: + * @cres: an #AscResult instance. + * @unit: an #AscUnit where release information could be read from. + * @cpt: the #AsComponent to read release data for. + * @mi_filename: the metadata filename for the component, as found in the passed unit. + * @allow_net: set to %TRUE if network access should be allowed. + * @acurl: the #AsCurl to use for network access. + * @used_reldata: (out) (optional): Receive the release data that was used, if set. + * + * Reads an external release description file, either from a network location or from + * the given unit. Also performs further processing on the release information, like sorting + * releases or pruning old ones. + **/ +void +asc_process_metainfo_releases (AscResult *cres, + AscUnit *unit, + AsComponent *cpt, + const gchar *mi_filename, + gboolean allow_net, + AsCurl *acurl, + GBytes **used_reldata) +{ + g_autoptr(GError) local_error = NULL; + g_autoptr(GBytes) relmd_bytes = NULL; + + /* download external release metadata or fetch local release data */ + if (as_component_get_releases_kind (cpt) == AS_RELEASES_KIND_EXTERNAL) { + g_autofree gchar *relmd_uri = NULL; + + g_ptr_array_set_size (as_component_get_releases (cpt), 0); + if (allow_net && as_component_get_releases_url (cpt) != NULL) { + /* get the release data from a network location */ + const gchar *releases_url = as_component_get_releases_url (cpt); + + relmd_bytes = as_curl_download_bytes (acurl, releases_url, &local_error); + if (relmd_bytes == NULL) { + asc_result_add_hint (cres, NULL, + "metainfo-releases-download-failed", + "url", releases_url, + "msg", local_error->message, + NULL); + goto out; + } + relmd_uri = g_strdup (releases_url); + } else { + /* we need to find local release information */ + g_autofree gchar *relfile_path = NULL; + g_autofree gchar *relfile_name = NULL; + g_autofree gchar *tmp = NULL; + + relfile_name = g_strconcat (as_component_get_id (cpt), ".releases.xml", NULL); + tmp = g_path_get_dirname (mi_filename); + relfile_path = g_build_filename (tmp, "releases", relfile_name, NULL); + + relmd_bytes = asc_unit_read_data (unit, relfile_path, &local_error); + if (relmd_bytes == NULL) { + asc_result_add_hint (cres, NULL, + "file-read-error", + "fname", relfile_path, + "msg", local_error->message, + NULL); + goto out; + } + relmd_uri = g_steal_pointer (&relfile_path); + } + + if (!as_component_load_releases_from_bytes (cpt, relmd_bytes, &local_error)) { + asc_result_add_hint (cres, NULL, + "metainfo-releases-read-failed", + "path", relmd_uri, + "msg", local_error->message, + NULL); + goto out; + } + } + + /* limit the amount of releases that we add to the output metadata. + * since releases are sorted with the newest one at the top, we will only + * remove the older ones. */ + if (as_component_get_kind (cpt) != AS_COMPONENT_KIND_OPERATING_SYSTEM) { + GPtrArray *releases = as_component_get_releases (cpt); + if (releases->len > MAX_RELEASE_INFO_COUNT) + g_ptr_array_set_size (releases, MAX_RELEASE_INFO_COUNT); + } + +out: + if (used_reldata != NULL) + *used_reldata = g_steal_pointer (&relmd_bytes); +} + /** * asc_validate_metainfo_data_for_component: * @cres: an #AscResult instance. @@ -139,64 +221,93 @@ asc_parse_metainfo_data_simple (AscResult *cres, GBytes *bytes, const gchar *mi_ * @cpt: the loaded #AsComponent which we are validating * @bytes: the data @cpt was constructed from. * @mi_basename: the basename of the MetaInfo file we are analyzing. + * @relmd_bytes: (nullable): the release metadata for this component. * * Validates MetaInfo data for the given component and stores the validation result as issue hints * in the given #AscResult. * Both the result as well as the validator's state may be modified by this function. **/ void -asc_validate_metainfo_data_for_component (AscResult *cres, AsValidator *validator, - AsComponent *cpt, GBytes *bytes, const gchar *mi_basename) +asc_validate_metainfo_data_for_component (AscResult *cres, + AsValidator *validator, + AsComponent *cpt, + GBytes *bytes, + const gchar *mi_basename, + GBytes *relmd_bytes) { - g_autoptr(GList) issues = NULL; + GHashTable *issues_files; + GHashTableIter hiter; + gpointer hkey, hvalue; /* don't check web URLs for validity, we catch those issues differently */ as_validator_set_check_urls (validator, FALSE); /* remove issues from a potential previous use of this validator */ as_validator_clear_issues (validator); + as_validator_clear_release_data (validator); + + /* add release data if we have any */ + if (relmd_bytes != NULL) { + g_autoptr(GError) tmp_error = NULL; + g_autofree gchar *release_name = g_strconcat (as_component_get_id (cpt), ".releases.xml", NULL); + if (!as_validator_add_release_bytes (validator, + release_name, + relmd_bytes, + &tmp_error)) + g_warning ("Failed to add release metadata for %s: %s", + as_component_get_id (cpt), tmp_error->message); + } /* validate */ as_validator_validate_bytes (validator, bytes); /* convert & register found issues */ - issues = as_validator_get_issues (validator); - for (GList *l = issues; l != NULL; l = l->next) { - g_autofree gchar *asv_tag = NULL; - g_autofree gchar *location = NULL; - glong line; - const gchar *issue_hint; - AsValidatorIssue *issue = AS_VALIDATOR_ISSUE (l->data); - - /* we have a special hint tag for legacy metadata, - * with its proper "error" priority */ - if (g_strcmp0 (as_validator_issue_get_tag (issue), "metainfo-ancient") == 0) { - asc_result_add_hint_simple (cres, cpt, "ancient-metadata"); - continue; + issues_files = as_validator_get_issues_per_file (validator); + g_hash_table_iter_init (&hiter, issues_files); + while (g_hash_table_iter_next (&hiter, &hkey, &hvalue)) { + const gchar *filename = (const gchar*) hkey; + const GPtrArray *issues = (const GPtrArray*) hvalue; + + if (filename == NULL) + filename = mi_basename; + + for (guint i = 0; i < issues->len; i++) { + g_autofree gchar *asv_tag = NULL; + g_autofree gchar *location = NULL; + glong line; + const gchar *issue_hint; + AsValidatorIssue *issue = AS_VALIDATOR_ISSUE (g_ptr_array_index (issues, i)); + + /* we have a special hint tag for legacy metadata, + * with its proper "error" priority */ + if (g_strcmp0 (as_validator_issue_get_tag (issue), "metainfo-ancient") == 0) { + asc_result_add_hint_simple (cres, cpt, "ancient-metadata"); + continue; + } + + /* create a tag for asgen out of the AppStream validator tag by prefixing it */ + asv_tag = g_strconcat ("asv-", + as_validator_issue_get_tag (issue), + NULL); + + line = as_validator_issue_get_line (issue); + if (line >= 0) + location = g_strdup_printf ("%s:%ld", filename, line); + else + location = g_strdup (filename); + + /* we don't need to do much here, with the tag generated here, + * the hint registry will automatically assign the right explanation + * text and severity to the issue. */ + issue_hint = as_validator_issue_get_hint (issue); + if (issue_hint == NULL) + issue_hint = ""; + asc_result_add_hint (cres, cpt, + asv_tag, + "location", location, + "hint", issue_hint, + NULL); } - - /* create a tag for asgen out of the AppStream validator tag by prefixing it */ - asv_tag = g_strconcat ("asv-", - as_validator_issue_get_tag (issue), - NULL); - - line = as_validator_issue_get_line (issue); - if (line >= 0) - location = g_strdup_printf ("%s:%ld", mi_basename, line); - else - location = g_strdup (mi_basename); - - /* we don't need to do much here, with the tag generated here, - * the hint registry will automatically assign the right explanation - * text and severity to the issue. */ - issue_hint = as_validator_issue_get_hint (issue); - if (issue_hint == NULL) - issue_hint = ""; - asc_result_add_hint (cres, cpt, - asv_tag, - "location", location, - "hint", issue_hint, - NULL); } } diff --git a/compose/asc-utils-metainfo.h b/compose/asc-utils-metainfo.h index 32a9df68..bf9d5bdb 100644 --- a/compose/asc-utils-metainfo.h +++ b/compose/asc-utils-metainfo.h @@ -24,6 +24,7 @@ #include #include "as-settings-private.h" +#include "as-curl.h" #include "asc-result.h" #include "asc-compose.h" @@ -37,12 +38,20 @@ AsComponent *asc_parse_metainfo_data (AscResult *cres, AsComponent *asc_parse_metainfo_data_simple (AscResult *cres, GBytes *bytes, const gchar *mi_basename); +void asc_process_metainfo_releases (AscResult *cres, + AscUnit *unit, + AsComponent *cpt, + const gchar *mi_filename, + gboolean allow_net, + AsCurl *acurl, + GBytes **used_reldata); void asc_validate_metainfo_data_for_component (AscResult *cres, AsValidator *validator, AsComponent *cpt, GBytes *bytes, - const gchar *mi_basename); + const gchar *mi_basename, + GBytes *relmd_bytes); AS_INTERNAL_VISIBLE AsComponent *asc_parse_desktop_entry_data (AscResult *cres, diff --git a/data/its/metainfo.its b/data/its/metainfo.its index 1fdc8738..ff48a6e2 100644 --- a/data/its/metainfo.its +++ b/data/its/metainfo.its @@ -35,6 +35,13 @@ + + + + + diff --git a/data/its/metainfo.loc b/data/its/metainfo.loc index d94ad6e6..5fa75485 100644 --- a/data/its/metainfo.loc +++ b/data/its/metainfo.loc @@ -5,10 +5,14 @@ SPDX-License-Identifier: FSFAP --> - + - + + + + + diff --git a/docs/xml/collection-xmldata.xml b/docs/xml/collection-xmldata.xml index 968a3154..5bbfeb9b 100644 --- a/docs/xml/collection-xmldata.xml +++ b/docs/xml/collection-xmldata.xml @@ -505,8 +505,7 @@ <releases/> - The releases tag and its release children are structured as described in , with the - additional requirement that releases must be sorted in a latest-to-oldest order. + The releases tag and its release children are structured as described in . Each release tag may have a description tag as child, containing a brief description of what is new in the release. @@ -517,6 +516,11 @@ It may also convert ISO 8601 date properties of the metainfo file into an UNIX timestamp timestamp property. It should avoid generating metadata containing both properties on a release tag. + + If a tag in a metainfo file references an external release description, the release description should + be read either from the release file provided locally, or, if possible and provided, be downloaded from the URL referenced in the component's releases + tag. + Example for a valid releases tag: diff --git a/docs/xml/metainfo-component.xml b/docs/xml/metainfo-component.xml index 714eab9b..1829a49c 100644 --- a/docs/xml/metainfo-component.xml +++ b/docs/xml/metainfo-component.xml @@ -499,11 +499,53 @@ <releases/> - The ]]> tag contains multiple release children that contain - metadata about releases made for this software component. + The ]]> tag contains multiple release children that + themselves contain metadata about releases made for this software component. The release information XML is described in-depth in , examples for a valid releases tag with artifacts are also provided there. + + Release information can be embedded in the component's metainfo file, following the XML + description outlined in . Alternatively, it can also + be split into its own metadata file as described in that section. In case of external metadata, + a releases tag must still be present in the component's metainfo file, and + must have a type property set to value external (if the type + property is missing, a value of embedded is implicitly assumed for it). + + + In case of external metadata, the releases tag may also have an url + property linking to a web location where the release XML can be found and updated separately from the + main component metadata. An url property must not be present without type + set to external. + + + Only HTTPS links are allowed for the web URL, and any artifact defined in a + release description from an external website should not be trusted without further verification, as external + release information can currently not be signed. + + + AppStream catalog metadata generators may choose to update the locally provided release information with the data from + the web location provided by the URL in url. + This allow projects to complete release localization after a release was made, or include further information that was + not yet available directly at release time. + The generated catalog XML data must be complete and must not contain references to external release information. + + + Example for a releases block that points to an external metadata file: + + ]]> + + Local Release Data + + Please note that even if release data is external and also provided on a remote location, it also must be + available locally, installed as a file into /usr/share/metainfo/releases/%{cid}.releases.xml. + The local file may not contain all information (for example it may not have a complete release description or all translations), + but basic data such as the released versions and their release dates should be present. + + + It is an error to reference an external release data file, but not provide a local copy of it. + + diff --git a/docs/xml/releases-data.xml b/docs/xml/releases-data.xml index e4d6ef3b..6ddd4eba 100644 --- a/docs/xml/releases-data.xml +++ b/docs/xml/releases-data.xml @@ -13,6 +13,27 @@ This section documents the tag that can be part of a component to provide information about releases made for the respective component. + + Alternatively to being embedded in a component metainfo file, the data may also be split into a dedicated XML file to be updated separately. + + + +
+ Locations + + + Release data may be present directly in a component metainfo file, but also optionally be split out into an external metadata file. + + + If the releases XML is part of a metainfo file, it is embedded into it following the semantics described in the document. + + + If the releases XML is external, the metainfo file must contain a tag with the type + property set to external as described for component XML. + The data described in this section is placed in a separate XML file with releases being its root node. + The file must be installed as /usr/share/metainfo/releases/%{cid}.releases.xml, where cid is the component ID of the component + the release information belongs to. +
diff --git a/src/as-component-private.h b/src/as-component-private.h index 260c1d41..f5cb973a 100644 --- a/src/as-component-private.h +++ b/src/as-component-private.h @@ -54,6 +54,9 @@ void as_component_complete (AsComponent *cpt, gchar *scr_base_url, GPtrArray *icon_paths); +gint as_component_releases_compare (gconstpointer a, + gconstpointer b); + AS_INTERNAL_VISIBLE GHashTable *as_component_get_languages_table (AsComponent *cpt); diff --git a/src/as-component.c b/src/as-component.c index 5e5070b9..00c3554d 100644 --- a/src/as-component.c +++ b/src/as-component.c @@ -79,6 +79,8 @@ typedef struct gchar *source_pkgname; GRefString *origin; GRefString *branch; + gchar *releases_url; + AsReleasesKind releases_kind; GHashTable *name; /* localized entry */ GHashTable *summary; /* localized entry */ @@ -335,6 +337,48 @@ as_component_scope_from_string (const gchar *scope_str) return AS_COMPONENT_SCOPE_UNKNOWN; } +/** + * as_releases_kind_to_string: + * @kind: the #AsReleaseKind. + * + * Converts the enumerated value to an text representation. + * + * Returns: string version of @kind + * + * Since: 0.16.0 + **/ +const gchar* +as_releases_kind_to_string (AsReleasesKind kind) +{ + if (kind == AS_RELEASES_KIND_EMBEDDED) + return "embedded"; + if (kind == AS_RELEASES_KIND_EXTERNAL) + return "external"; + return "unknown"; +} + +/** + * as_releases_kind_from_string: + * @kind_str: the string. + * + * Converts the text representation to an enumerated value. + * + * Returns: an #AsReleaseKind or %AS_RELEASE_KIND_UNKNOWN for unknown + * + * Since: 0.16.0 + **/ +AsReleasesKind +as_releases_kind_from_string (const gchar *kind_str) +{ + if (as_is_empty (kind_str)) + return AS_RELEASES_KIND_EMBEDDED; + if (as_str_equal0 (kind_str, "embedded")) + return AS_RELEASES_KIND_EMBEDDED; + if (as_str_equal0 (kind_str, "external")) + return AS_RELEASES_KIND_EXTERNAL; + return AS_RELEASES_KIND_UNKNOWN; +} + /** * as_component_init: **/ @@ -392,6 +436,7 @@ as_component_init (AsComponent *cpt) g_free); priv->priority = 0; + priv->releases_kind = AS_RELEASES_KIND_EMBEDDED; } /** @@ -415,6 +460,7 @@ as_component_finalize (GObject* object) as_ref_string_release (priv->arch); as_ref_string_release (priv->origin); as_ref_string_release (priv->branch); + g_free (priv->releases_url); g_hash_table_unref (priv->name); g_hash_table_unref (priv->summary); @@ -565,6 +611,140 @@ as_component_add_screenshot (AsComponent *cpt, AsScreenshot* sshot) g_ptr_array_add (sslist, g_object_ref (sshot)); } +/** + * as_component_load_releases_from_bytes: + * @cpt: a #AsComponent instance. + * @bytes: the release XML data as #GBytes + * @error: a #GError. + * + * Load release information from XML bytes. + * + * Returns: %TRUE on success. + * + * Since: 0.16.0 + **/ +gboolean +as_component_load_releases_from_bytes (AsComponent *cpt, GBytes *bytes, GError **error) +{ + AsComponentPrivate *priv = GET_PRIVATE (cpt); + const gchar *rel_data = NULL; + gsize rel_data_len; + xmlDoc *xdoc; + xmlNode *xroot; + GError *tmp_error = NULL; + + rel_data = g_bytes_get_data (bytes, &rel_data_len); + xdoc = as_xml_parse_document (rel_data, rel_data_len, &tmp_error); + if (xdoc == NULL) { + g_propagate_prefixed_error (error, + tmp_error, + "Unable to parse external release data: "); + return FALSE; + } + + /* load releases */ + xroot = xmlDocGetRootElement (xdoc); + for (xmlNode *iter = xroot->children; iter != NULL; iter = iter->next) { + if (iter->type != XML_ELEMENT_NODE) + continue; + if (as_str_equal0 (iter->name, "release")) { + g_autoptr(AsRelease) release = as_release_new (); + if (as_release_load_from_xml (release, priv->context, iter, NULL)) + g_ptr_array_add (priv->releases, g_steal_pointer (&release)); + } + } + xmlFreeDoc (xdoc); + + return TRUE; +} + +/** + * as_component_load_releases: + * @cpt: a #AsComponent instance. + * @reload: set to %TRUE to discard existing data and reload. + * @allow_net: allow fetching release data from the internet. + * @error: a #GError. + * + * Load data from an external source, possibly a local file + * or a network resource. + * + * Returns: %TRUE on success. + * + * Since: 0.16.0 + **/ +gboolean +as_component_load_releases (AsComponent *cpt, gboolean reload, gboolean allow_net, GError **error) +{ + AsComponentPrivate *priv = GET_PRIVATE (cpt); + g_autoptr(GBytes) reldata_bytes = NULL; + GError *tmp_error = NULL; + + if (priv->releases_kind != AS_RELEASES_KIND_EXTERNAL) + return TRUE; + if (priv->releases->len != 0 && !reload) + return TRUE; + + /* we need context data for this to work properly */ + if (priv->context == NULL) { + g_set_error_literal (error, + AS_UTILS_ERROR, + AS_UTILS_ERROR_FAILED, + "Unable to read external release information from a component without metadata context."); + return FALSE; + } + + if (reload) + g_ptr_array_set_size (priv->releases, 0); + + if (allow_net && priv->releases_url != NULL) { + /* grab release data from a remote source */ + g_autoptr(AsCurl) curl = NULL; + + curl = as_context_get_curl (priv->context, error); + if (curl == NULL) + return FALSE; + + reldata_bytes = as_curl_download_bytes (curl, priv->releases_url, &tmp_error); + if (reldata_bytes == NULL) { + g_propagate_prefixed_error (error, + tmp_error, + "Unable to obtain remote external release data: "); + return FALSE; + } + } else { + /* read release data from a local source */ + g_autofree gchar *relfile_path = NULL; + g_autofree gchar *relfile_name = NULL; + g_autofree gchar *tmp = NULL; + gchar *rel_data = NULL; + gsize rel_data_len; + const gchar *mi_fname = NULL; + + mi_fname = as_context_get_filename (priv->context); + if (mi_fname == NULL) { + g_set_error_literal (error, + AS_UTILS_ERROR, + AS_UTILS_ERROR_FAILED, + "Unable to read external release information: Component has no known metainfo filename."); + return FALSE; + } + relfile_name = g_strconcat (priv->id, ".releases.xml", NULL); + tmp = g_path_get_dirname (mi_fname); + relfile_path = g_build_filename (tmp, "releases", relfile_name, NULL); + + if (!g_file_get_contents (relfile_path, &rel_data, &rel_data_len, &tmp_error)) { + g_propagate_prefixed_error (error, + tmp_error, + "Unable to read local external release data: "); + return FALSE; + } + + reldata_bytes = g_bytes_new_take (rel_data, rel_data_len); + } + + return as_component_load_releases_from_bytes (cpt, reldata_bytes, error); +} + /** * as_component_get_releases: * @cpt: a #AsComponent instance. @@ -578,6 +758,11 @@ GPtrArray* as_component_get_releases (AsComponent *cpt) { AsComponentPrivate *priv = GET_PRIVATE (cpt); + g_autoptr(GError) error = NULL; + + if (!as_component_load_releases (cpt, FALSE, FALSE, &error)) + g_debug ("Error loading data for %s: %s", + as_component_get_data_id (cpt), error->message); return priv->releases; } @@ -595,6 +780,74 @@ as_component_add_release (AsComponent *cpt, AsRelease* release) g_ptr_array_add (priv->releases, g_object_ref (release)); } +/** + * as_component_get_releases_kind: + * @cpt: a #AsComponent instance. + * + * Returns the #AsReleasesKind of the release metadata + * associated with this component. + * + * Returns: The kind. + * + * Since: 0.16.0 + */ +AsReleasesKind +as_component_get_releases_kind (AsComponent *cpt) +{ + AsComponentPrivate *priv = GET_PRIVATE (cpt); + return priv->releases_kind; +} + +/** + * as_component_set_releases_kind: + * @cpt: a #AsComponent instance. + * @kind: the #AsComponentKind. + * + * Sets the #AsReleasesKind of the release metadata + * associated with this component. + * + * Since: 0.16.0 + */ +void +as_component_set_releases_kind (AsComponent *cpt, AsReleasesKind kind) +{ + AsComponentPrivate *priv = GET_PRIVATE (cpt); + priv->releases_kind = kind; +} + +/** + * as_component_get_releases_url: + * @cpt: a #AsComponent instance. + * + * Get a remote URL to obtain release information for the component. + * + * Returns: The URL of external release data. + * + * Since: 0.16.0 + **/ +const gchar* +as_component_get_releases_url (AsComponent *cpt) +{ + AsComponentPrivate *priv = GET_PRIVATE (cpt); + return priv->releases_url; +} + +/** + * as_component_set_releases_url: + * @cpt: a #AsComponent instance. + * @url: the web URL where release data is found. + * + * Set a remote URL pointing to an AppStream release info file. + * + * Since: 0.16.0 + **/ +void +as_component_set_releases_url (AsComponent *cpt, const gchar *url) +{ + AsComponentPrivate *priv = GET_PRIVATE (cpt); + as_assign_string_safe (priv->releases_url, url); +} + /** * as_component_get_url: * @cpt: a #AsComponent instance. @@ -4099,15 +4352,15 @@ as_component_load_keywords_from_xml (AsComponent *cpt, AsContext *ctx, xmlNode * } /** - * as_component_releases_sort_cb: + * as_component_releases_compare: * * Callback for releases #GPtrArray sorting. * * NOTE: We sort in descending order here, so the most recent * release ends up at the top of the list. */ -static gint -as_component_releases_sort_cb (gconstpointer a, gconstpointer b) +gint +as_component_releases_compare (gconstpointer a, gconstpointer b) { AsRelease **rel1 = (AsRelease **) a; AsRelease **rel2 = (AsRelease **) b; @@ -4129,7 +4382,7 @@ static void as_component_sort_releases (AsComponent *cpt) { AsComponentPrivate *priv = GET_PRIVATE (cpt); - g_ptr_array_sort (priv->releases, as_component_releases_sort_cb); + g_ptr_array_sort (priv->releases, as_component_releases_compare); } /** @@ -4313,13 +4566,30 @@ as_component_load_from_xml (AsComponent *cpt, AsContext *ctx, xmlNode *node, GEr if (content != NULL) as_component_set_compulsory_for_desktop (cpt, content); } else if (tag_id == AS_TAG_RELEASES) { - for (xmlNode *iter2 = iter->children; iter2 != NULL; iter2 = iter2->next) { - if (iter2->type != XML_ELEMENT_NODE) - continue; - if (g_strcmp0 ((const gchar*) iter2->name, "release") == 0) { - g_autoptr(AsRelease) release = as_release_new (); - if (as_release_load_from_xml (release, ctx, iter2, NULL)) - g_ptr_array_add (priv->releases, g_steal_pointer (&release)); + g_autofree gchar *releases_kind_str = as_xml_get_prop_value (iter, "type"); + priv->releases_kind = as_releases_kind_from_string (releases_kind_str); + if (priv->releases_kind == AS_RELEASES_KIND_EXTERNAL) { + g_autofree gchar *release_url_prop = as_xml_get_prop_value (iter, "url"); + if (release_url_prop != NULL) { + g_free (priv->releases_url); + /* handle the media baseurl */ + if (as_context_has_media_baseurl (ctx)) + priv->releases_url = g_strconcat (as_context_get_media_baseurl (ctx), "/", release_url_prop, NULL); + else + priv->releases_url = g_steal_pointer (&release_url_prop); + } + } + + /* only read release data if it is not external */ + if (priv->releases_kind != AS_RELEASES_KIND_EXTERNAL) { + for (xmlNode *iter2 = iter->children; iter2 != NULL; iter2 = iter2->next) { + if (iter2->type != XML_ELEMENT_NODE) + continue; + if (g_strcmp0 ((const gchar*) iter2->name, "release") == 0) { + g_autoptr(AsRelease) release = as_release_new (); + if (as_release_load_from_xml (release, ctx, iter2, NULL)) + g_ptr_array_add (priv->releases, g_steal_pointer (&release)); + } } } } else if (tag_id == AS_TAG_EXTENDS) { @@ -4823,7 +5093,12 @@ as_component_to_xml_node (AsComponent *cpt, AsContext *ctx, xmlNode *root) as_branding_to_xml_node (priv->branding, ctx, cnode); /* releases */ - if (priv->releases->len > 0) { + if (priv->releases_kind == AS_RELEASES_KIND_EXTERNAL && as_context_get_style (ctx) == AS_FORMAT_STYLE_METAINFO) { + xmlNode *rnode = as_xml_add_node (cnode, "releases"); + as_xml_add_text_prop (rnode, "type", "external"); + if (priv->releases_url != NULL) + as_xml_add_text_prop (rnode, "url", priv->releases_url); + } else if (priv->releases->len > 0) { xmlNode *rnode = as_xml_add_node (cnode, "releases"); /* ensure releases are sorted, then emit XML nodes */ @@ -6171,7 +6446,7 @@ as_component_to_xml_data (AsComponent *cpt, AsContext *context, GError **error) g_return_val_if_fail (context != NULL, NULL); node = as_component_to_xml_node (cpt, context, NULL); - return as_xml_node_to_str (node, error); + return as_xml_node_free_to_str (node, error); } /** diff --git a/src/as-component.h b/src/as-component.h index 76e08f95..46cc745d 100644 --- a/src/as-component.h +++ b/src/as-component.h @@ -198,6 +198,27 @@ typedef enum /*< skip >*/ __attribute__((__packed__)) { /* DEPRECATED */ #define AS_SEARCH_TOKEN_MATCH_MIMETYPE AS_SEARCH_TOKEN_MATCH_MEDIATYPE +/** + * AsReleasesKind: + * @AS_RELEASES_KIND_UNKNOWN: Unknown releases type + * @AS_RELEASES_KIND_EMBEDDED: Release info is embedded in metainfo file + * @AS_RELEASES_KIND_EXTERNAL: Release info is split to a separate file + * + * The kind of a releases block. + * + * Since: 0.16.0 + **/ +typedef enum { + AS_RELEASES_KIND_UNKNOWN, + AS_RELEASES_KIND_EMBEDDED, + AS_RELEASES_KIND_EXTERNAL, + /*< private >*/ + AS_RELEASES_KIND_LAST +} AsReleasesKind; + +const gchar *as_releases_kind_to_string (AsReleasesKind kind); +AsReleasesKind as_releases_kind_from_string (const gchar *kind_str); + AsComponent *as_component_new (void); AsValueFlags as_component_get_value_flags (AsComponent *cpt); @@ -330,9 +351,24 @@ void as_component_add_url (AsComponent *cpt, AsUrlKind url_kind, const gchar *url); +gboolean as_component_load_releases_from_bytes (AsComponent *cpt, + GBytes *bytes, + GError **error); +gboolean as_component_load_releases (AsComponent *cpt, + gboolean reload, + gboolean allow_net, + GError **error); GPtrArray *as_component_get_releases (AsComponent *cpt); void as_component_add_release (AsComponent *cpt, - AsRelease* release); + AsRelease* release); + +AsReleasesKind as_component_get_releases_kind (AsComponent *cpt); +void as_component_set_releases_kind (AsComponent *cpt, + AsReleasesKind kind); + +const gchar *as_component_get_releases_url (AsComponent *cpt); +void as_component_set_releases_url (AsComponent *cpt, + const gchar *url); GPtrArray *as_component_get_extends (AsComponent *cpt); void as_component_add_extends (AsComponent *cpt, diff --git a/src/as-context-private.h b/src/as-context-private.h index 9dc39e03..29446a68 100644 --- a/src/as-context-private.h +++ b/src/as-context-private.h @@ -23,6 +23,7 @@ #include "as-context.h" #include "as-component.h" +#include "as-curl.h" G_BEGIN_DECLS #pragma GCC visibility push(hidden) @@ -44,6 +45,9 @@ void as_context_localized_ht_set (AsContext *ctx, const gchar *value, const gchar *locale); +AsCurl *as_context_get_curl (AsContext *ctx, + GError **error); + #pragma GCC visibility pop G_END_DECLS diff --git a/src/as-context.c b/src/as-context.c index 8ebfa3c3..f21a63e2 100644 --- a/src/as-context.c +++ b/src/as-context.c @@ -52,6 +52,9 @@ typedef struct gboolean internal_mode; gboolean all_locale; + + AsCurl *curl; + GMutex mutex; } AsContextPrivate; G_DEFINE_TYPE_WITH_PRIVATE (AsContext, as_context, G_TYPE_OBJECT) @@ -146,6 +149,10 @@ as_context_finalize (GObject *object) as_ref_string_release (priv->media_baseurl); as_ref_string_release (priv->arch); as_ref_string_release (priv->fname); + g_mutex_clear (&priv->mutex); + + if (priv->curl != NULL) + g_object_unref (priv->curl); G_OBJECT_CLASS (as_context_parent_class)->finalize (object); } @@ -155,6 +162,7 @@ as_context_init (AsContext *ctx) { AsContextPrivate *priv = GET_PRIVATE (ctx); + g_mutex_init (&priv->mutex); priv->format_version = AS_FORMAT_VERSION_CURRENT; priv->style = AS_FORMAT_STYLE_UNKNOWN; priv->priority = 0; @@ -527,6 +535,28 @@ as_context_localized_ht_set (AsContext *ctx, GHashTable *lht, const gchar *value g_strdup (value)); } +/** + * as_context_get_curl: + * @ctx: a #AsContext instance, or %NULL + * @error: a #GError + * + * Get an #AsCurl instance. + * + * Returns: (transfer full): an #AsCurl reference. + */ +AsCurl* +as_context_get_curl (AsContext *ctx, GError **error) +{ + AsContextPrivate *priv = GET_PRIVATE (ctx); + g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->mutex); + if (priv->curl == NULL) { + priv->curl = as_curl_new (error); + if (priv->curl == NULL) + return NULL; + } + return g_object_ref (priv->curl); +} + /** * as_context_new: * diff --git a/src/as-metadata.c b/src/as-metadata.c index 32a1dd61..5b9ad2cc 100644 --- a/src/as-metadata.c +++ b/src/as-metadata.c @@ -40,6 +40,7 @@ #include "as-utils-private.h" #include "as-component.h" #include "as-component-private.h" +#include "as-release-private.h" #include "as-context-private.h" #include "as-distro-details.h" #include "as-desktop-entry.h" @@ -414,7 +415,11 @@ as_metadata_yaml_parse_collection_doc (AsMetadata *metad, AsContext *context, co * Parses AppStream metadata. **/ static gboolean -as_metadata_parse_data (AsMetadata *metad, const gchar *data, gssize data_len, AsFormatKind format, GError **error) +as_metadata_parse_data (AsMetadata *metad, + const gchar *data, gssize data_len, + AsFormatKind format, + const gchar *filename, + GError **error) { AsMetadataPrivate *priv = GET_PRIVATE (metad); g_return_val_if_fail (format > AS_FORMAT_KIND_UNKNOWN && format < AS_FORMAT_KIND_LAST, FALSE); @@ -430,7 +435,7 @@ as_metadata_parse_data (AsMetadata *metad, const gchar *data, gssize data_len, A if (priv->mode == AS_FORMAT_STYLE_CATALOG) { /* prepare context */ - g_autoptr(AsContext) context = as_metadata_new_context (metad, AS_FORMAT_STYLE_CATALOG, NULL); + g_autoptr(AsContext) context = as_metadata_new_context (metad, AS_FORMAT_STYLE_CATALOG, filename); if (g_strcmp0 ((gchar*) root->name, "components") == 0) { as_metadata_xml_parse_components_node (metad, context, root, error); @@ -451,7 +456,7 @@ as_metadata_parse_data (AsMetadata *metad, const gchar *data, gssize data_len, A g_autoptr(AsContext) context = NULL; g_autoptr(AsComponent) cpt = NULL; - context = as_metadata_new_context (metad, AS_FORMAT_STYLE_METAINFO, NULL); + context = as_metadata_new_context (metad, AS_FORMAT_STYLE_METAINFO, filename); if (priv->update_existing) { /* we should update the existing component with new metadata */ cpt = as_metadata_get_component (metad); @@ -487,7 +492,7 @@ as_metadata_parse_data (AsMetadata *metad, const gchar *data, gssize data_len, A g_autoptr(GPtrArray) new_cpts = NULL; guint i; - context = as_metadata_new_context (metad, AS_FORMAT_STYLE_CATALOG, NULL); + context = as_metadata_new_context (metad, AS_FORMAT_STYLE_CATALOG, filename); new_cpts = as_metadata_yaml_parse_collection_doc (metad, context, data, @@ -544,6 +549,7 @@ as_metadata_parse (AsMetadata *metad, const gchar *data, AsFormatKind format, GE data, -1, format, + NULL, error); } @@ -569,6 +575,7 @@ as_metadata_parse_bytes (AsMetadata *metad, GBytes *bytes, AsFormatKind format, data, data_len, format, + NULL, error); } @@ -650,6 +657,7 @@ gboolean as_metadata_parse_file (AsMetadata *metad, GFile *file, AsFormatKind format, GError **error) { g_autofree gchar *file_basename = NULL; + g_autofree gchar *filename = NULL; g_autoptr(GFileInfo) info = NULL; g_autoptr(GInputStream) file_stream = NULL; g_autoptr(GInputStream) stream_data = NULL; @@ -669,6 +677,7 @@ as_metadata_parse_file (AsMetadata *metad, GFile *file, AsFormatKind format, GEr content_type = g_file_info_get_attribute_string (info, G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE); file_basename = g_file_get_basename (file); + filename = g_file_get_path (file); if (format == AS_FORMAT_KIND_UNKNOWN) { /* we should autodetect the format type. assume XML until we can find evidence that it's YAML */ format = AS_FORMAT_KIND_XML; @@ -730,6 +739,7 @@ as_metadata_parse_file (AsMetadata *metad, GFile *file, AsFormatKind format, GEr asdata->str, asdata->len, format, + filename, &tmp_error); if (tmp_error != NULL) { g_propagate_error (error, tmp_error); @@ -738,6 +748,124 @@ as_metadata_parse_file (AsMetadata *metad, GFile *file, AsFormatKind format, GEr return TRUE; } +/** + * as_metadata_parse_releases_bytes: + * @metad: An instance of #AsMetadata. + * @bytes: Metadata describing release notes. + * @error: A #GError or %NULL. + * + * Parses any AppStream release metadata into #AsRelease objects. + * + * Returns: (element-type AsRelease) (transfer container) (nullable): A list of releases or %NULL on error. + * + * Since: 0.16.0 + **/ +GPtrArray* +as_metadata_parse_releases_bytes (AsMetadata *metad, GBytes *bytes, GError **error) +{ + g_autoptr(GPtrArray) releases = NULL; + g_autoptr(AsContext) context = NULL; + xmlDoc *xdoc; + xmlNode *xroot; + gsize data_len; + const gchar *data = g_bytes_get_data (bytes, &data_len); + + xdoc = as_xml_parse_document (data, data_len, error); + if (xdoc == NULL) + return NULL; + + context = as_metadata_new_context (metad, AS_FORMAT_STYLE_METAINFO, NULL); + releases = g_ptr_array_new_with_free_func (g_object_unref); + + /* load releases */ + xroot = xmlDocGetRootElement (xdoc); + for (xmlNode *iter = xroot->children; iter != NULL; iter = iter->next) { + if (iter->type != XML_ELEMENT_NODE) + continue; + if (as_str_equal0 (iter->name, "release")) { + g_autoptr(AsRelease) release = as_release_new (); + if (as_release_load_from_xml (release, context, iter, NULL)) + g_ptr_array_add (releases, g_steal_pointer (&release)); + } + } + xmlFreeDoc (xdoc); + + return g_steal_pointer (&releases); +} + +/** + * as_metadata_parse_releases_file: + * @metad: A valid #AsMetadata instance + * @file: #GFile for the release metadata + * @error: A #GError or %NULL. + * + * Parses any AppStream release metadata into #AsRelease objects + * using the provided file. + * + * Returns: (element-type AsRelease) (transfer container) (nullable): A list of releases or %NULL on error. + * + * Since: 0.16.0 + **/ +GPtrArray* +as_metadata_parse_releases_file (AsMetadata *metad, GFile *file, GError **error) +{ + g_autoptr(GFileInputStream) input_stream = NULL; + g_autoptr(GByteArray) byte_array = NULL; + g_autoptr(GBytes) bytes = NULL; + gsize bytes_read; + + input_stream = g_file_read (file, NULL, error); + if (input_stream == NULL) + return NULL; + + byte_array = g_byte_array_new (); + do { + guint8 buffer[1024]; + if (!g_input_stream_read_all (G_INPUT_STREAM(input_stream), + buffer, sizeof(buffer), + &bytes_read, + NULL, + error)) + return NULL; + + if (bytes_read > 0) + g_byte_array_append (byte_array, buffer, bytes_read); + } while (bytes_read > 0); + + bytes = g_byte_array_free_to_bytes (g_steal_pointer (&byte_array)); + return as_metadata_parse_releases_bytes (metad, bytes, error); +} + +/** + * as_metadata_releases_to_data: + * @metad: A valid #AsMetadata instance + * @releases: (element-type AsRelease): the list of #Asrelease to convert. + * @error: A #GError or %NULL. + * + * Convert a list of #Asrelease entities into a release metadata XML representation. + * + * Returns: The XML representation or %NULL on error. + * + * Since: 0.16.0 + **/ +gchar* +as_metadata_releases_to_data (AsMetadata *metad, GPtrArray *releases, GError **error) +{ + xmlNode *root; + g_autoptr(AsContext) context = NULL; + + root = as_xml_node_new ("releases"); + context = as_metadata_new_context (metad, AS_FORMAT_STYLE_METAINFO, NULL); + + g_ptr_array_sort (releases, as_component_releases_compare); + for (guint i = 0; i < releases->len; i++) { + AsRelease *rel = AS_RELEASE (g_ptr_array_index (releases, i)); + as_release_to_xml_node (rel, context, root); + } + + return as_xml_node_free_to_str (root, error); +} + /** * as_metadata_save_data: */ @@ -915,7 +1043,7 @@ as_metadata_component_to_metainfo (AsMetadata *metad, AsFormatKind format, GErro return NULL; node = as_component_to_xml_node (cpt, context, NULL); - xmlstr = as_xml_node_to_str (node, error); + xmlstr = as_xml_node_free_to_str (node, error); return xmlstr; } @@ -930,9 +1058,8 @@ as_metadata_xml_serialize_to_collection_with_rootnode (AsMetadata *metad, AsCont { AsMetadataPrivate *priv = GET_PRIVATE (metad); xmlNode *root; - guint i; - root = xmlNewNode (NULL, (xmlChar*) "components"); + root = as_xml_node_new ("components"); as_xml_add_text_prop (root, "version", as_format_version_to_string (priv->format_version)); @@ -949,7 +1076,7 @@ as_metadata_xml_serialize_to_collection_with_rootnode (AsMetadata *metad, AsCont "media_baseurl", as_context_get_media_baseurl (context)); - for (i = 0; i < cpts->len; i++) { + for (guint i = 0; i < cpts->len; i++) { xmlNode *node; AsComponent *cpt = AS_COMPONENT (g_ptr_array_index (cpts, i)); @@ -959,7 +1086,7 @@ as_metadata_xml_serialize_to_collection_with_rootnode (AsMetadata *metad, AsCont xmlAddChild (root, node); } - return as_xml_node_to_str (root, NULL); + return as_xml_node_free_to_str (root, NULL); } /** diff --git a/src/as-metadata.h b/src/as-metadata.h index fad3a53c..aa162e45 100644 --- a/src/as-metadata.h +++ b/src/as-metadata.h @@ -107,6 +107,16 @@ gboolean as_metadata_parse_desktop_data (AsMetadata *metad, const gchar *cid, GError **error); +GPtrArray *as_metadata_parse_releases_bytes (AsMetadata *metad, + GBytes *bytes, + GError **error); +GPtrArray *as_metadata_parse_releases_file (AsMetadata *metad, + GFile *file, + GError **error); +gchar *as_metadata_releases_to_data (AsMetadata *metad, + GPtrArray *releases, + GError **error); + AsComponent *as_metadata_get_component (AsMetadata *metad); GPtrArray *as_metadata_get_components (AsMetadata *metad); diff --git a/src/as-news-convert.c b/src/as-news-convert.c index 596d0a30..a3968c6a 100644 --- a/src/as-news-convert.c +++ b/src/as-news-convert.c @@ -111,7 +111,7 @@ as_releases_to_metainfo_xml_chunk (GPtrArray *releases, GError **error) n_releases); } - xml_raw = as_xml_node_to_str (root, error); + xml_raw = as_xml_node_free_to_str (root, error); if ((error != NULL) && (*error != NULL)) return NULL; diff --git a/src/as-validator-issue-tag.h b/src/as-validator-issue-tag.h index cb6df0aa..0fbb986e 100644 --- a/src/as-validator-issue-tag.h +++ b/src/as-validator-issue-tag.h @@ -821,6 +821,28 @@ AsValidatorIssueTag as_validator_issue_tag_list[] = { "Sorting releases also increases overall readability of the metainfo file."), }, + { "releases-type-invalid", + AS_ISSUE_SEVERITY_ERROR, + /* TRANSLATORS: Please do not translate AppStream tag/property names (in backticks). */ + N_("The type of the releases block is invalid. It needs to either `embedded` (the default) or `external`."), + }, + + { "releases-url-insecure", + AS_ISSUE_SEVERITY_ERROR, + N_("The URL to an external release metadata file is insecure. This is not allowed, please use HTTPS URLs only."), + }, + + { "releases-download-failed", + AS_ISSUE_SEVERITY_ERROR, + N_("Failed to download release metadata."), + }, + + { "releases-external-not-found", + AS_ISSUE_SEVERITY_WARNING, + N_("A local release metadata file was not found. It is strongly recommended to validate this metadata " + "together with the main MetaInfo file."), + }, + { "release-urgency-invalid", AS_ISSUE_SEVERITY_WARNING, N_("The value set as release urgency is not a known urgency value."), diff --git a/src/as-validator.c b/src/as-validator.c index 43813f13..04ab8a01 100644 --- a/src/as-validator.c +++ b/src/as-validator.c @@ -59,6 +59,8 @@ typedef struct AsComponent *current_cpt; gchar *current_fname; + gchar *current_dir; + GPtrArray *release_data; /* of AsReleaseDataPair */ gboolean check_urls; gboolean strict; @@ -78,6 +80,29 @@ G_DEFINE_TYPE_WITH_PRIVATE (AsValidator, as_validator, G_TYPE_OBJECT) G_DEFINE_QUARK (as-validator-error-quark, as_validator_error) +typedef struct { + gchar *fname; + GBytes *bytes; +} AsReleaseDataPair; + +static AsReleaseDataPair* +as_release_data_pair_new (const gchar *fname, GBytes *bytes) +{ + AsReleaseDataPair *pair; + pair = g_new0 (AsReleaseDataPair, 1); + pair->fname = g_strdup (fname); + pair->bytes = g_bytes_ref (bytes); + return pair; +} + +static void +as_release_data_pair_free (AsReleaseDataPair *pair) +{ + g_free (pair->fname); + g_bytes_unref (pair->bytes); + g_free (pair); +} + /** * as_validator_init: **/ @@ -108,6 +133,9 @@ as_validator_init (AsValidator *validator) g_str_equal, g_free, (GDestroyNotify) g_ptr_array_unref); + /* registry for injected release metadata */ + priv->release_data = g_ptr_array_new_with_free_func ((GDestroyNotify) as_release_data_pair_free); + priv->current_fname = NULL; priv->current_cpt = NULL; priv->check_urls = FALSE; @@ -129,8 +157,10 @@ as_validator_finalize (GObject *object) g_hash_table_unref (priv->issues); g_free (priv->current_fname); + g_free (priv->current_dir); if (priv->current_cpt != NULL) g_object_unref (priv->current_cpt); + g_ptr_array_unref (priv->release_data); if (priv->acurl != NULL) g_object_unref (priv->acurl); @@ -211,7 +241,7 @@ as_validator_add_issue (AsValidator *validator, xmlNode *node, const gchar *tag, /** * as_validator_set_current_fname: * - * Sets the name of the file we are currently dealing with. + * Sets the basename of the file we are currently dealing with. **/ static void as_validator_set_current_fname (AsValidator *validator, const gchar *fname) @@ -221,6 +251,19 @@ as_validator_set_current_fname (AsValidator *validator, const gchar *fname) priv->current_fname = g_strdup (fname); } +/** + * as_validator_set_current_dir: + * + * Sets the path to the directory with the metainfo file that we are currently dealing with. + **/ +static void +as_validator_set_current_dir (AsValidator *validator, const gchar *dirname) +{ + AsValidatorPrivate *priv = GET_PRIVATE (validator); + g_free (priv->current_dir); + priv->current_dir = g_strdup (dirname); +} + /** * as_validator_clear_current_fname: * @@ -383,6 +426,120 @@ as_validator_check_web_url (AsValidator *validator, xmlNode *node, const gchar * return TRUE; } +/** + * as_validator_clear_release_data: + * @validator: a #AsValidator instance. + * + * Clear all release information that was explicitly added to the + * validation process. + * + * Since: 0.16.0 + */ +void +as_validator_clear_release_data (AsValidator *validator) +{ + AsValidatorPrivate *priv = GET_PRIVATE (validator); + g_ptr_array_set_size (priv->release_data, 0); +} + +/** + * as_validator_add_release_bytes: + * @validator: a #AsValidator instance. + * @release_fname: File basename of the release metadata file to add. + * @release_metadata: Data of the release metadata file. + * @error: a #GError or %NULL + * + * Add release metadata explicitly from bytes. + * + * Since: 0.16.0 + */ +gboolean +as_validator_add_release_bytes (AsValidator *validator, + const gchar *release_fname, + GBytes *release_metadata, + GError **error) +{ + AsValidatorPrivate *priv = GET_PRIVATE (validator); + + /* sanity check */ + if (!g_str_has_suffix (release_fname, ".releases.xml") && + !g_str_has_suffix (release_fname, ".releases.xml.in")) { + g_set_error (error, + AS_VALIDATOR_ERROR, + AS_VALIDATOR_ERROR_INVALID_FILENAME, + _("The release metadata file '%s' is named incorrectly."), + release_fname); + return FALSE; + } + if (g_strstr_len (release_fname, -1, "/") != NULL) { + g_set_error (error, + AS_VALIDATOR_ERROR, + AS_VALIDATOR_ERROR_INVALID_FILENAME, + "Expected a basename for release file '%s', but got a full path instead.", + release_fname); + return FALSE; + } + + g_ptr_array_add (priv->release_data, + as_release_data_pair_new (release_fname, release_metadata)); + return TRUE; +} + +/** + * as_validator_add_release_file: + * @validator: a #AsValidator instance. + * @release_file: Release metadata file to add. + * @error: a #GError or %NULL + * + * Add a release metadata file to the validation process. + * + * Since: 0.16.0 + */ +gboolean +as_validator_add_release_file (AsValidator *validator, GFile *release_file, GError **error) +{ + AsValidatorPrivate *priv = GET_PRIVATE (validator); + g_autoptr(GFileInputStream) input_stream = NULL; + g_autoptr(GByteArray) byte_array = NULL; + g_autoptr(GBytes) bytes = NULL; + g_autofree gchar *basename = NULL; + gsize bytes_read; + + basename = g_file_get_basename (release_file); + if (!g_str_has_suffix (basename, ".releases.xml") && + !g_str_has_suffix (basename, ".releases.xml.in")) { + g_set_error (error, + AS_VALIDATOR_ERROR, + AS_VALIDATOR_ERROR_INVALID_FILENAME, + _("The release metadata file '%s' is named incorrectly."), + basename); + return FALSE; + } + + input_stream = g_file_read (release_file, NULL, error); + if (input_stream == NULL) + return FALSE; + + byte_array = g_byte_array_new (); + do { + guint8 buffer[1024]; + if (!g_input_stream_read_all (G_INPUT_STREAM(input_stream), + buffer, sizeof(buffer), + &bytes_read, + NULL, + error)) + return FALSE; + + if (bytes_read > 0) + g_byte_array_append (byte_array, buffer, bytes_read); + } while (bytes_read > 0); + + bytes = g_byte_array_free_to_bytes (g_steal_pointer (&byte_array)); + g_ptr_array_add (priv->release_data, + as_release_data_pair_new (basename, bytes)); + return TRUE; +} + /** * as_validator_get_check_urls: * @validator: a #AsValidator instance. @@ -1932,10 +2089,78 @@ as_validator_check_release (AsValidator *validator, xmlNode *node, AsFormatStyle } /** - * as_validator_check_releases: + * as_validator_find_release_data_for_current: + * + * Find release metadata for the current component, if any data was provided. + */ +static AsReleaseDataPair* +as_validator_find_release_data_for_current (AsValidator *validator) +{ + AsValidatorPrivate *priv = GET_PRIVATE (validator); + g_autofree gchar *expected_name = NULL; + const gchar *cid = as_component_get_id (priv->current_cpt); + + expected_name = g_strconcat (cid, ".releases.xml", NULL); + for (guint i = 0; i < priv->release_data->len; i++) { + AsReleaseDataPair *pair = g_ptr_array_index (priv->release_data, i); + if (g_str_has_prefix (pair->fname, expected_name)) + return pair; + } + + /* it's not explicitly provided, try to cheat and apply some heuristics */ + if (priv->current_dir != NULL) { + g_autofree gchar *guessed_path = NULL; + gchar *contents; + gsize contents_len; + g_autoptr(GError) error = NULL; + + guessed_path = g_build_filename (priv->current_dir, "releases", expected_name, NULL); + g_debug ("Trying to find release metadata in %s", guessed_path); + if (!g_file_test (guessed_path, G_FILE_TEST_EXISTS)) { + g_free (guessed_path); + guessed_path = g_build_filename (priv->current_dir, expected_name, NULL); + g_debug ("Trying to find release metadata in %s", guessed_path); + if (!g_file_test (guessed_path, G_FILE_TEST_EXISTS)) { + g_autofree gchar *tmp = g_strconcat (expected_name, ".in", NULL); + guessed_path = g_build_filename (priv->current_dir, tmp, NULL); + g_debug ("Trying to find release metadata in %s", guessed_path); + if (!g_file_test (guessed_path, G_FILE_TEST_EXISTS)) + g_free (g_steal_pointer (&guessed_path)); + } + } + + if (guessed_path == NULL) + return NULL; + + if (g_file_get_contents (guessed_path, &contents, &contents_len, &error)) { + AsReleaseDataPair *pair; + g_autofree gchar *basename = NULL; + g_autoptr(GBytes) bytes = g_bytes_new_take (contents, contents_len); + basename = g_path_get_basename (guessed_path); + pair = as_release_data_pair_new (basename, bytes); + g_ptr_array_add (priv->release_data, pair); + return pair; + } else { + g_autofree gchar *cpt_fname = g_steal_pointer (&priv->current_fname); + as_validator_set_current_fname (validator, expected_name); + + as_validator_add_issue (validator, NULL, + "file-read-failed", + error->message); + + /* restore currently analyzed file name */ + as_validator_set_current_fname (validator, cpt_fname); + } + } + + return NULL; +} + +/** + * as_validator_check_releases_node: **/ static void -as_validator_check_releases (AsValidator *validator, xmlNode *node, AsFormatStyle mode) +as_validator_check_releases_node (AsValidator *validator, xmlNode *node, AsFormatStyle mode) { for (xmlNode *iter = node->children; iter != NULL; iter = iter->next) { const gchar *node_name; @@ -1959,6 +2184,111 @@ as_validator_check_releases (AsValidator *validator, xmlNode *node, AsFormatStyl } } +/** + * as_validator_check_external_releases: + **/ +static void +as_validator_check_external_releases (AsValidator *validator, xmlNode *rels_node, GBytes *bytes, const gchar *releases_uri, AsFormatStyle mode) +{ + AsValidatorPrivate *priv = GET_PRIVATE (validator); + const gchar *rel_data = NULL; + gsize rel_data_len; + xmlDoc *xdoc; + xmlNode *xroot; + g_autofree gchar *rel_basename = NULL; + g_autoptr(GError) error = NULL; + g_autofree gchar *cpt_fname = g_steal_pointer (&priv->current_fname); + + if (g_str_has_prefix (releases_uri, "http")) + rel_basename = g_strdup (releases_uri); + else + rel_basename = g_path_get_basename (releases_uri); + as_validator_set_current_fname (validator, rel_basename); + + rel_data = g_bytes_get_data (bytes, &rel_data_len); + xdoc = as_xml_parse_document (rel_data, rel_data_len, &error); + if (xdoc == NULL) { + as_validator_add_issue (validator, rels_node, + "xml-markup-invalid", error->message); + goto out; + } + + /* check remote releases */ + xroot = xmlDocGetRootElement (xdoc); + as_validator_check_releases_node (validator, xroot, mode); + xmlFreeDoc (xdoc); + +out: + /* restore currently analyzed file name */ + as_validator_set_current_fname (validator, cpt_fname); +} + +/** + * as_validator_check_releases: + **/ +static void +as_validator_check_releases (AsValidator *validator, xmlNode *node, AsFormatStyle mode) +{ + AsValidatorPrivate *priv = GET_PRIVATE (validator); + AsReleasesKind releases_kind; + AsReleaseDataPair *rel_pair; + g_autofree gchar *release_url_prop = NULL; + g_autofree gchar *releases_kind_str = as_xml_get_prop_value (node, "type"); + releases_kind = as_releases_kind_from_string (releases_kind_str); + + if (releases_kind == AS_RELEASES_KIND_UNKNOWN) { + as_validator_add_issue (validator, node, + "releases-type-invalid", releases_kind_str); + } + + if (releases_kind != AS_RELEASES_KIND_EXTERNAL) { + as_validator_check_releases_node (validator, node, mode); + return; + } + + /* if we are here, we have external release metadata and need to find and validate it */ + + release_url_prop = as_xml_get_prop_value (node, "url"); + if (release_url_prop != NULL) { + if (!g_str_has_prefix (release_url_prop, "https:")) + as_validator_add_issue (validator, node, + "releases-url-insecure", release_url_prop); + + /* only download & validate the file if network access is allowed */ + if (priv->check_urls) { + g_autoptr(GBytes) bytes = NULL; + GError *tmp_error = NULL; + + g_debug ("Downloading release metadata: %s", release_url_prop); + bytes = as_curl_download_bytes (priv->acurl, release_url_prop, &tmp_error); + if (bytes == NULL) { + as_validator_add_issue (validator, node, + "releases-download-failed", tmp_error->message); + } else { + as_validator_check_external_releases (validator, + node, + bytes, + release_url_prop, + mode); + } + } + } + + rel_pair = as_validator_find_release_data_for_current (validator); + if (rel_pair == NULL) { + as_validator_add_issue (validator, node, + "releases-external-not-found", NULL); + return; + } + + as_validator_check_external_releases (validator, + node, + rel_pair->bytes, + rel_pair->fname, + mode); + as_component_load_releases_from_bytes (priv->current_cpt, rel_pair->bytes, NULL); +} + /** * as_validator_check_branding: **/ @@ -2622,6 +2952,8 @@ as_validator_validate_file (AsValidator *validator, GFile *metadata_file) g_autoptr(GString) asxmldata = NULL; g_autoptr(GBytes) bytes = NULL; g_autofree gchar *fname = NULL; + g_autofree gchar *dirname = NULL; + g_autofree gchar *tmp = NULL; gssize len; const gsize buffer_size = 1024 * 32; g_autofree gchar *buffer = NULL; @@ -2637,7 +2969,10 @@ as_validator_validate_file (AsValidator *validator, GFile *metadata_file) content_type = g_file_info_get_attribute_string (info, G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE); fname = g_file_get_basename (metadata_file); + tmp = g_file_get_path (metadata_file); + dirname = g_path_get_dirname (tmp); as_validator_set_current_fname (validator, fname); + as_validator_set_current_dir (validator, dirname); file_stream = G_INPUT_STREAM (g_file_read (metadata_file, NULL, &tmp_error)); if (tmp_error != NULL) { @@ -3142,6 +3477,23 @@ as_validator_validate_tree (AsValidator *validator, const gchar *root_dir) return ret && as_validator_check_success (validator); } +/** + * as_validator_get_issue_files_count: + * @validator: An instance of #AsValidator. + * + * Get the number of files for which issues have been found. + * + * Returns: The number of files that have issues. + * + * Since: 0.16.0 + */ +guint +as_validator_get_issue_files_count (AsValidator *validator) +{ + AsValidatorPrivate *priv = GET_PRIVATE (validator); + return g_hash_table_size (priv->issues_per_file); +} + /** * as_validator_get_issues: * @validator: An instance of #AsValidator. diff --git a/src/as-validator.h b/src/as-validator.h index 19832afd..c6ceaf96 100644 --- a/src/as-validator.h +++ b/src/as-validator.h @@ -49,12 +49,14 @@ struct _AsValidatorClass * AsValidatorError: * @AS_VALIDATOR_ERROR_FAILED: Generic failure * @AS_VALIDATOR_ERROR_OVERRIDE_INVALID: The issue override was not accepted. + * @AS_VALIDATOR_ERROR_INVALID_FILENAME: The filename was invalid. * * The error type. **/ typedef enum { AS_VALIDATOR_ERROR_FAILED, AS_VALIDATOR_ERROR_OVERRIDE_INVALID, + AS_VALIDATOR_ERROR_INVALID_FILENAME, /*< private >*/ AS_VALIDATOR_ERROR_LAST } AsValidatorError; @@ -66,7 +68,7 @@ AsValidator *as_validator_new (void); void as_validator_clear_issues (AsValidator *validator); gboolean as_validator_validate_file (AsValidator *validator, - GFile* metadata_file); + GFile *metadata_file); gboolean as_validator_validate_bytes (AsValidator *validator, GBytes *metadata); gboolean as_validator_validate_data (AsValidator *validator, @@ -74,6 +76,16 @@ gboolean as_validator_validate_data (AsValidator *validator, gboolean as_validator_validate_tree (AsValidator *validator, const gchar *root_dir); +void as_validator_clear_release_data (AsValidator *validator); +gboolean as_validator_add_release_bytes (AsValidator *validator, + const gchar *release_fname, + GBytes *release_metadata, + GError **error); +gboolean as_validator_add_release_file (AsValidator *validator, + GFile *release_file, + GError **error); + +guint as_validator_get_issue_files_count (AsValidator *validator); GList *as_validator_get_issues (AsValidator *validator); GHashTable *as_validator_get_issues_per_file (AsValidator *validator); gboolean as_validator_get_report_yaml (AsValidator *validator, diff --git a/src/as-xml.c b/src/as-xml.c index 0936841a..d31523c7 100644 --- a/src/as-xml.c +++ b/src/as-xml.c @@ -1089,7 +1089,7 @@ as_xml_parse_document (const gchar *data, gssize len, GError **error) } /** - * as_xml_node_to_str: + * as_xml_node_free_to_str: * @root: The document root node. * * Converts an XML node into its textural representation. @@ -1099,7 +1099,7 @@ as_xml_parse_document (const gchar *data, gssize len, GError **error) * Returns: XML metadata. */ gchar* -as_xml_node_to_str (xmlNode *root, GError **error) +as_xml_node_free_to_str (xmlNode *root, GError **error) { xmlDoc *doc; gchar *xmlstr = NULL; diff --git a/src/as-xml.h b/src/as-xml.h index 9fc05fb7..f89ced33 100644 --- a/src/as-xml.h +++ b/src/as-xml.h @@ -89,6 +89,7 @@ void as_xml_add_custom_node (xmlNode *root, const gchar *node_name, GHashTable *custom); +#define as_xml_node_new(name) xmlNewNode (NULL, (xmlChar*) name) #define as_xml_add_node(root, name) xmlNewChild (root, NULL, (xmlChar*) name, NULL) xmlNode *as_xml_add_text_node (xmlNode *root, const gchar *name, @@ -101,7 +102,8 @@ xmlDoc *as_xml_parse_document (const gchar *data, gssize len, GError **error); -gchar *as_xml_node_to_str (xmlNode *root, GError **error); +gchar *as_xml_node_free_to_str (xmlNode *root, + GError **error); #pragma GCC visibility pop G_END_DECLS diff --git a/tests/samples/org.example.pomidaq.metainfo.xml b/tests/samples/org.example.pomidaq.metainfo.xml new file mode 100644 index 00000000..bed44d86 --- /dev/null +++ b/tests/samples/org.example.pomidaq.metainfo.xml @@ -0,0 +1,35 @@ + + + org.example.pomidaq + PoMiDAQ + View and record videos from UCLA Miniscopes + FSFAP + LGPL-3.0+ + +

+ PoMiDAQ is a recording software for UCLA Miniscopes for neuroscientific research. + It provides an easy way to record videos from Miniscopes, create Z-stacks to give an + overview of visible cells or tissue features and provides an online background subtraction + feature to gain an initial insight into cellular calcium activity while data is recorded. + Recorded data is encoded with the FFV1 codec by default, to allow for smaller, + lossless video files that are safe to archive. +

+
+ pomidaq.desktop + https://github.com/bothlab/pomidaq + https://github.com/bothlab/pomidaq/issues + + + Recording a raw video of calcium activity while displaying the difference to the average background + https://raw.githubusercontent.com/bothlab/pomidaq/master/contrib/screenshots/v0.4.4_diffdisplay.png + + + Displaying the raw image data recorded from a GRIN lens in CA1 of a mouse hippocampus + https://raw.githubusercontent.com/bothlab/pomidaq/master/contrib/screenshots/v0.4.4_rawdisplay.png + + + + + + +
diff --git a/tests/samples/releases/org.example.pomidaq.releases.xml b/tests/samples/releases/org.example.pomidaq.releases.xml new file mode 100644 index 00000000..f7148b90 --- /dev/null +++ b/tests/samples/releases/org.example.pomidaq.releases.xml @@ -0,0 +1,63 @@ + + + + +

This release adds the following features:

+
    +
  • Display device name for camera ID on Linux
  • +
  • Don't store data in temporary location by default
  • +
  • Update screenshots
  • +
  • Add Flatpak bundle build recipe
  • +
  • Validate & augment metainfo file using appstreamcli
  • +
+

This release fixes the following bugs:

+
    +
  • Prevent in-tree builds
  • +
  • Give a better error when trying to build against GLES instead of OpenGL
  • +
  • cmake: Find more recent FFmpeg versions as well
  • +
+
+
+ + +

This release adds the following features:

+
    +
  • Implement z-stack capture feature for Miniscopes with an EWL
  • +
  • Run display image rendering as event loop callback
  • +
+

This release fixes the following bug:

+
    +
  • Resolve a few compiler warnings
  • +
+
+
+ + +

This release adds the following feature:

+
    +
  • Make build instructions a bit more beginner-friendly
  • +
+

This release fixes the following bugs:

+
    +
  • Give video display container a minimum width
  • +
  • Don't use deprecated FFmpeg API
  • +
  • Save view splitter sizes as byte array, instead of variant
  • +
+
+
+ + +

This release adds the following features:

+
    +
  • Add spin boxes for sliding values, in addition to slider widgets
  • +
  • Start Miniscopes on highest framerate by default
  • +
+

This release fixes the following bugs:

+
    +
  • Tell DAQ board when we are recording data
  • +
  • Consistently sort Miniscope controls
  • +
  • Save & restore main window splitter position
  • +
+
+
+
diff --git a/tests/test-xmldata.c b/tests/test-xmldata.c index af19aa86..1ca57ef6 100644 --- a/tests/test-xmldata.c +++ b/tests/test-xmldata.c @@ -98,6 +98,20 @@ as_xml_test_compare_xml (const gchar *result, const gchar *expected) return as_test_compare_lines (result, expected_full); } +/** + * test_log_allow_warnings: + * + * Helper function to temporarily allow warnings to be non-fatal. + */ +static gboolean +test_log_allow_warnings (const gchar *log_domain, + GLogLevelFlags log_level, + const gchar *message, + gpointer user_data) +{ + return ((log_level & G_LOG_LEVEL_MASK) <= G_LOG_LEVEL_CRITICAL); +} + /** * test_appstream_parser_legacy: * @@ -1776,6 +1790,7 @@ test_xml_read_releases (void) g_assert_cmpstr (as_component_get_id (cpt), ==, "org.example.ReleaseTest"); g_assert_cmpint (as_component_get_releases (cpt)->len, ==, 1); + g_assert_cmpint (as_component_get_releases_kind (cpt), ==, AS_RELEASES_KIND_EMBEDDED); rel = AS_RELEASE (g_ptr_array_index (as_component_get_releases (cpt), 0)); g_assert_cmpint (as_release_get_kind (rel), ==, AS_RELEASE_KIND_STABLE); @@ -2085,6 +2100,65 @@ test_xml_rw_branding (void) g_assert_true (as_xml_test_compare_xml (res, xmldata_tags)); } +/** + * test_xml_rw_external_releases: + */ +static void +test_xml_rw_external_releases (void) +{ + static const gchar *xmldata_tags = + "\n" + " org.example.ExternalReleaseTest\n" + " \n" + "\n"; + g_autoptr(AsComponent) cpt = NULL; + g_autoptr(AsMetadata) metad = NULL; + g_autoptr(GFile) file = NULL; + g_autofree gchar *path = NULL; + g_autofree gchar *res = NULL; + GPtrArray *releases; + g_autoptr(GError) error = NULL; + + /* read */ + cpt = as_xml_test_read_data (xmldata_tags, AS_FORMAT_STYLE_METAINFO); + g_assert_cmpstr (as_component_get_id (cpt), ==, "org.example.ExternalReleaseTest"); + + /* validate */ + + /* we ignore warnings, as this will throw one since we can not find the external release info file */ + g_test_log_set_fatal_handler (test_log_allow_warnings, NULL); + releases = as_component_get_releases (cpt); + g_test_log_set_fatal_handler (NULL, NULL); + + g_assert_nonnull (releases); + g_assert_cmpint (releases->len, ==, 0); + + g_assert_cmpstr (as_component_get_releases_url (cpt), ==, "https://example.com/releases/test.releases.xml"); + g_assert_cmpint (as_component_get_releases_kind (cpt), ==, AS_RELEASES_KIND_EXTERNAL); + + /* write */ + res = as_xml_test_serialize (cpt, AS_FORMAT_STYLE_METAINFO); + g_assert_true (as_xml_test_compare_xml (res, xmldata_tags)); + + /* test reading from file */ + metad = as_metadata_new (); + + path = g_build_filename (datadir, "org.example.pomidaq.metainfo.xml", NULL); + file = g_file_new_for_path (path); + + as_metadata_parse_file (metad, file, AS_FORMAT_KIND_XML, &error); + g_assert_no_error (error); + g_object_unref (g_steal_pointer (&cpt)); + cpt = g_object_ref (as_metadata_get_component (metad)); + + g_assert_cmpstr (as_component_get_id (cpt), ==, "org.example.pomidaq"); + releases = as_component_get_releases (cpt); + g_assert_nonnull (releases); + g_assert_cmpint (releases->len, ==, 4); + g_assert_cmpstr (as_component_get_releases_url (cpt), ==, "https://raw.githubusercontent.com/ximion/appstream/master/tests/samples/releases/org.example.pomidaq.releases.xml"); + g_assert_cmpint (as_component_get_releases_kind (cpt), ==, AS_RELEASES_KIND_EXTERNAL); +} + /** * main: */ @@ -2156,6 +2230,7 @@ main (int argc, char **argv) g_test_add_func ("/XML/ReadWrite/Reviews", test_xml_rw_reviews); g_test_add_func ("/XML/ReadWrite/Tags", test_xml_rw_tags); g_test_add_func ("/XML/ReadWrite/Branding", test_xml_rw_branding); + g_test_add_func ("/XML/ReadWrite/ExternalReleases", test_xml_rw_external_releases); ret = g_test_run (); g_free (datadir); diff --git a/tools/ascli-actions-validate.c b/tools/ascli-actions-validate.c index a388c96b..f28c8c50 100644 --- a/tools/ascli-actions-validate.c +++ b/tools/ascli-actions-validate.c @@ -166,6 +166,68 @@ print_single_issue (AsValidatorIssue *issue, return no_errors; } +/** + * ascli_print_validation_result: + */ +static gboolean +ascli_print_validation_result (AsValidator *validator, + gboolean pedantic, + gboolean explain, + gboolean strict, + gboolean always_print_fnames, + gulong *error_count, + gulong *warning_count, + gulong *info_count, + gulong *pedantic_count) +{ + GHashTable *issues_files; + GHashTableIter hiter; + gpointer hkey, hvalue; + gboolean print_filenames; + gboolean validation_passed = TRUE; + + print_filenames = as_validator_get_issue_files_count (validator) > 1 || always_print_fnames; + issues_files = as_validator_get_issues_per_file (validator); + + g_hash_table_iter_init (&hiter, issues_files); + while (g_hash_table_iter_next (&hiter, &hkey, &hvalue)) { + const gchar *filename = (const gchar*) hkey; + const GPtrArray *issues = (const GPtrArray*) hvalue; + + if (print_filenames) { + if (filename == NULL) + filename = ""; + if (ascli_get_output_colored ()) + g_print ("%c[%dm%s%c[%dm\n", 0x1B, 1, filename, 0x1B, 0); + else + g_print ("%s\n", filename); + } + + for (guint i = 0; i < issues->len; i++) { + AsValidatorIssue *issue = AS_VALIDATOR_ISSUE (g_ptr_array_index (issues, i)); + + if (!print_single_issue (issue, + pedantic, + explain, + print_filenames? 2 : 0, + error_count, + warning_count, + info_count, + pedantic_count)) + validation_passed = FALSE; + + if (strict && as_validator_issue_get_severity (issue) != AS_ISSUE_SEVERITY_PEDANTIC) + validation_passed = FALSE; + } + + /* space out contents from different files a bit more if we only show tags */ + if (!explain) + g_print("\n"); + } + + return validation_passed; +} + /** * ascli_validate_apply_overrides_from_string: * @@ -210,7 +272,8 @@ ascli_validate_apply_overrides_from_string (AsValidator *validator, const gchar * ascli_validate_file: **/ static gboolean -ascli_validate_file (gchar *fname, +ascli_validate_file (AsValidator *validator, + const gchar *fname, gboolean print_filename, gboolean pedantic, gboolean explain, @@ -224,9 +287,6 @@ ascli_validate_file (gchar *fname, { GFile *file; gboolean validation_passed = TRUE; - AsValidator *validator; - GList *issues; - GList *l; file = g_file_new_for_path (fname); if (!g_file_query_exists (file, NULL)) { @@ -236,7 +296,6 @@ ascli_validate_file (gchar *fname, return FALSE; } - validator = as_validator_new (); as_validator_set_check_urls (validator, use_net); as_validator_set_strict (validator, validate_strict); @@ -244,37 +303,21 @@ ascli_validate_file (gchar *fname, if (!ascli_validate_apply_overrides_from_string (validator, overrides_str)) return FALSE; - /* validate ! */ + /* validate! */ if (!as_validator_validate_file (validator, file)) validation_passed = FALSE; - issues = as_validator_get_issues (validator); - - if (print_filename) { - if (ascli_get_output_colored ()) - g_print ("%c[%dm%s%c[%dm\n", 0x1B, 1, fname, 0x1B, 0); - else - g_print ("%s\n", fname); - } - - for (l = issues; l != NULL; l = l->next) { - AsValidatorIssue *issue = AS_VALIDATOR_ISSUE (l->data); - if (!print_single_issue (issue, - pedantic, - explain, - print_filename? 2 : 0, - error_count, - warning_count, - info_count, - pedantic_count)) - validation_passed = FALSE; - if (validate_strict && as_validator_issue_get_severity (issue) != AS_ISSUE_SEVERITY_PEDANTIC) - validation_passed = FALSE; - } + validation_passed = ascli_print_validation_result (validator, + pedantic, + explain, + validate_strict, + print_filename, + error_count, + warning_count, + info_count, + pedantic_count)? validation_passed : FALSE; - g_list_free (issues); g_object_unref (file); - g_object_unref (validator); return validation_passed; } @@ -337,31 +380,49 @@ ascli_validate_files (gchar **argv, gulong warning_count = 0; gulong info_count = 0; gulong pedantic_count = 0; + g_autoptr(AsValidator) validator = NULL; + g_autoptr(GPtrArray) metainfo_files = NULL; if (argc < 1) { - g_print ("%s\n", _("You need to specify at least one file to validate!")); - return 1; + g_printerr ("%s\n", _("You need to specify at least one file to validate!")); + return ASCLI_EXIT_CODE_FAILED; } + validator = as_validator_new (); + metainfo_files = g_ptr_array_new (); for (gint i = 0; i < argc; i++) { - gboolean tmp_ret; - tmp_ret = ascli_validate_file (argv[i], - argc >= 2, /* print filenames if we validate multiple files */ - pedantic, - explain, - validate_strict, - use_net, - overrides_str, - &error_count, - &warning_count, - &info_count, - &pedantic_count); - if (!tmp_ret) - ret = FALSE; + if (g_strstr_len (argv[i], -1, ".releases.xml") != NULL) { + g_autoptr(GError) local_error = NULL; + g_autoptr(GFile) file = g_file_new_for_path (argv[i]); + if (!as_validator_add_release_file (validator, file, &local_error)) { + ascli_print_stderr (_("Unable to add release metadata file: %s"), + local_error->message); + return ASCLI_EXIT_CODE_FATAL; + } + } else { + g_ptr_array_add (metainfo_files, argv[i]); + } + } + if (metainfo_files->len == 0) { + g_printerr ("%s\n", _("You need to specify at least one MetaInfo file to validate.\n" + "Release metadata files can currently not be validated without their accompanying MetaInfo file.")); + return ASCLI_EXIT_CODE_FAILED; + } - /* space out contents from different files a bit more if we only show tags */ - if (!explain) - g_print("\n"); + for (guint i = 0; i < metainfo_files->len; i++) { + const gchar *fname = (const gchar*) g_ptr_array_index (metainfo_files, i); + ret = ascli_validate_file (validator, + fname, + metainfo_files->len >= 2, /* print filenames if we validate multiple files */ + pedantic, + explain, + validate_strict, + use_net, + overrides_str, + &error_count, + &warning_count, + &info_count, + &pedantic_count)? ret : FALSE; } if (ret) { @@ -378,7 +439,7 @@ ascli_validate_files (gchar **argv, g_print ("\n"); } - return 0; + return ASCLI_EXIT_CODE_SUCCESS; } else { g_autofree gchar *tmp = g_strdup_printf (_("Validation failed: %s"), ""); g_print ("✘ %s", tmp); @@ -388,7 +449,7 @@ ascli_validate_files (gchar **argv, pedantic_count); g_print ("\n"); - return 3; + return ASCLI_EXIT_CODE_BAD_INPUT; } } @@ -469,9 +530,6 @@ ascli_validate_tree (const gchar *root_dir, { gboolean validation_passed = TRUE; AsValidator *validator; - GHashTable *issues_files; - GHashTableIter hiter; - gpointer hkey, hvalue; gulong error_count = 0; gulong warning_count = 0; gulong info_count = 0; @@ -490,41 +548,15 @@ ascli_validate_tree (const gchar *root_dir, return 1; as_validator_validate_tree (validator, root_dir); - issues_files = as_validator_get_issues_per_file (validator); - - g_hash_table_iter_init (&hiter, issues_files); - while (g_hash_table_iter_next (&hiter, &hkey, &hvalue)) { - const gchar *filename = (const gchar*) hkey; - const GPtrArray *issues = (const GPtrArray*) hvalue; - - if (filename != NULL) { - if (ascli_get_output_colored ()) - g_print ("%c[%dm%s%c[%dm\n", 0x1B, 1, filename, 0x1B, 0); - else - g_print ("%s\n", filename); - } - - for (guint i = 0; i < issues->len; i++) { - AsValidatorIssue *issue = AS_VALIDATOR_ISSUE (g_ptr_array_index (issues, i)); - - if (!print_single_issue (issue, - pedantic, - explain, - 2, - &error_count, - &warning_count, - &info_count, - &pedantic_count)) - validation_passed = FALSE; - - if (validate_strict && as_validator_issue_get_severity (issue) != AS_ISSUE_SEVERITY_PEDANTIC) - validation_passed = FALSE; - } - - /* space out contents from different files a bit more if we only show tags */ - if (!explain) - g_print("\n"); - } + validation_passed = ascli_print_validation_result (validator, + pedantic, + explain, + validate_strict, + TRUE, /* always print filenames */ + &error_count, + &warning_count, + &info_count, + &pedantic_count); g_object_unref (validator); if (validation_passed) {