/** * as_validator_validate_file: * @validator: An instance of #AsValidator. * @metadata_file: An AppStream XML file. * * Validate an AppStream XML file **/ gboolean as_validator_validate_file (AsValidator *validator, GFile *metadata_file) { g_autoptr(GFileInfo) info = NULL; g_autoptr(GInputStream) file_stream = NULL; g_autoptr(GInputStream) stream_data = NULL; g_autoptr(GConverter) conv = NULL; g_autoptr(GString) asxmldata = NULL; g_autofree gchar *fname = NULL; gssize len; const gsize buffer_size = 1024 * 32; g_autofree gchar *buffer = NULL; const gchar *content_type = NULL; g_autoptr(GError) tmp_error = NULL; gboolean ret; info = g_file_query_info (metadata_file, G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE, G_FILE_QUERY_INFO_NONE, NULL, NULL); if (info != NULL) content_type = g_file_info_get_attribute_string (info, G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE); fname = g_file_get_basename (metadata_file); as_validator_set_current_fname (validator, fname); file_stream = G_INPUT_STREAM (g_file_read (metadata_file, NULL, &tmp_error)); if (tmp_error != NULL) { as_validator_add_issue (validator, NULL, AS_ISSUE_IMPORTANCE_ERROR, AS_ISSUE_KIND_READ_ERROR, "Unable to read file: %s", tmp_error->message); return FALSE; } if (file_stream == NULL) return FALSE; if ((g_strcmp0 (content_type, "application/gzip") == 0) || (g_strcmp0 (content_type, "application/x-gzip") == 0)) { /* decompress the GZip stream */ conv = G_CONVERTER (g_zlib_decompressor_new (G_ZLIB_COMPRESSOR_FORMAT_GZIP)); stream_data = g_converter_input_stream_new (file_stream, conv); } else { stream_data = g_object_ref (file_stream); } asxmldata = g_string_new (""); buffer = g_malloc (buffer_size); while ((len = g_input_stream_read (stream_data, buffer, buffer_size, NULL, &tmp_error)) > 0) { g_string_append_len (asxmldata, buffer, len); } if (tmp_error != NULL) { as_validator_add_issue (validator, NULL, AS_ISSUE_IMPORTANCE_ERROR, AS_ISSUE_KIND_READ_ERROR, "Unable to read file: %s", tmp_error->message); return FALSE; } /* check if there was an error */ if (len < 0) return FALSE; ret = as_validator_validate_data (validator, asxmldata->str); as_validator_clear_current_fname (validator); return ret; }
/** * as_validator_analyze_component_metainfo_relation_cb: * * Helper function for GHashTable foreach iteration. */ static void as_validator_analyze_component_metainfo_relation_cb (const gchar *fname, AsComponent *cpt, struct MInfoCheckData *data) { g_autofree gchar *cid_base = NULL; /* if we have no component-id, we can't check anything */ if (as_component_get_id (cpt) == NULL) return; as_validator_set_current_cpt (data->validator, cpt); as_validator_set_current_fname (data->validator, fname); /* check if the fname and the component-id match */ if (g_str_has_suffix (as_component_get_id (cpt), ".desktop")) { cid_base = g_strndup (as_component_get_id (cpt), g_strrstr (as_component_get_id (cpt), ".") - as_component_get_id (cpt)); } else { cid_base = g_strdup (as_component_get_id (cpt)); } if (!as_matches_metainfo (fname, cid_base)) { /* the name-without-type didn't match - check for the full id in the component name */ if (!as_matches_metainfo (fname, as_component_get_id (cpt))) { as_validator_add_issue (data->validator, NULL, AS_ISSUE_IMPORTANCE_WARNING, AS_ISSUE_KIND_WRONG_NAME, "The metainfo filename does not match the component ID."); } } /* check if the referenced .desktop file exists */ if (as_component_get_kind (cpt) == AS_COMPONENT_KIND_DESKTOP_APP) { if (g_hash_table_contains (data->desktop_fnames, as_component_get_desktop_id (cpt))) { g_autofree gchar *desktop_fname_full = NULL; g_autoptr(GKeyFile) dfile = NULL; GError *tmp_error = NULL; desktop_fname_full = g_build_filename (data->apps_dir, as_component_get_desktop_id (cpt), NULL); dfile = g_key_file_new (); g_key_file_load_from_file (dfile, desktop_fname_full, G_KEY_FILE_NONE, &tmp_error); if (tmp_error != NULL) { as_validator_add_issue (data->validator, NULL, AS_ISSUE_IMPORTANCE_WARNING, AS_ISSUE_KIND_READ_ERROR, "Unable to read associated .desktop file: %s", tmp_error->message); g_error_free (tmp_error); tmp_error = NULL; } else { /* we successfully opened the .desktop file, now perform some checks */ /* name */ if (as_str_empty (as_component_get_name (cpt)) && (!g_key_file_has_key (dfile, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_NAME, NULL))) { /* we don't have a summary, and there is also none in the .desktop file - this is bad. */ as_validator_add_issue (data->validator, NULL, AS_ISSUE_IMPORTANCE_ERROR, AS_ISSUE_KIND_VALUE_MISSING, "The component is missing a name (none found in its metainfo or .desktop file)"); } /* summary */ if (as_str_empty (as_component_get_summary (cpt)) && (!g_key_file_has_key (dfile, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_COMMENT, NULL))) { /* we don't have a summary, and there is also none in the .desktop file - this is bad. */ as_validator_add_issue (data->validator, NULL, AS_ISSUE_IMPORTANCE_ERROR, AS_ISSUE_KIND_VALUE_MISSING, "The component is missing a summary (none found in its metainfo or .desktop file)"); } /* categories */ if (g_key_file_has_key (dfile, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_CATEGORIES, NULL)) { g_autofree gchar *cats_str = NULL; g_auto(GStrv) cats = NULL; guint i; cats_str = g_key_file_get_string (dfile, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_CATEGORIES, NULL); cats = g_strsplit (cats_str, ";", -1); for (i = 0; cats[i] != NULL; i++) { if (as_str_empty (cats[i])) continue; if (!as_utils_is_category_name (cats[i])) { as_validator_add_issue (data->validator, NULL, AS_ISSUE_IMPORTANCE_ERROR, AS_ISSUE_KIND_VALUE_WRONG, "The category '%s' defined in the .desktop file does not exist.", cats[i]); } } } } } else { as_validator_add_issue (data->validator, NULL, AS_ISSUE_IMPORTANCE_ERROR, AS_ISSUE_KIND_FILE_MISSING, "Component metadata refers to a non-existing .desktop file."); } } as_validator_clear_current_cpt (data->validator); as_validator_clear_current_fname (data->validator); }
/** * as_validator_validate_tree: * @validator: An instance of #AsValidator. * @root_dir: The root directory of the filesystem tree that should be validated. * * Validate a full directory tree for issues in AppStream metadata. **/ gboolean as_validator_validate_tree (AsValidator *validator, const gchar *root_dir) { g_autofree gchar *metainfo_dir = NULL; g_autofree gchar *legacy_metainfo_dir = NULL; g_autofree gchar *apps_dir = NULL; g_autoptr(GPtrArray) mfiles = NULL; g_autoptr(GPtrArray) mfiles_legacy = NULL; g_autoptr(GPtrArray) dfiles = NULL; GHashTable *dfilenames = NULL; GHashTable *validated_cpts = NULL; guint i; gboolean ret = TRUE; g_autoptr(AsXMLData) xdt = NULL; struct MInfoCheckData ht_helper; /* cleanup */ as_validator_clear_issues (validator); metainfo_dir = g_build_filename (root_dir, "usr", "share", "metainfo", NULL); legacy_metainfo_dir = g_build_filename (root_dir, "usr", "share", "appdata", NULL); apps_dir = g_build_filename (root_dir, "usr", "share", "applications", NULL); /* check if we actually have a directory which could hold metadata */ if ((!g_file_test (metainfo_dir, G_FILE_TEST_IS_DIR)) && (!g_file_test (legacy_metainfo_dir, G_FILE_TEST_IS_DIR))) { as_validator_add_issue (validator, NULL, AS_ISSUE_IMPORTANCE_INFO, AS_ISSUE_KIND_FILE_MISSING, "No AppStream metadata was found."); goto out; } /* check if we actually have a directory which could hold application information */ if (!g_file_test (apps_dir, G_FILE_TEST_IS_DIR)) { as_validator_add_issue (validator, NULL, AS_ISSUE_IMPORTANCE_PEDANTIC, /* pedantic because not everything which has metadata is an application */ AS_ISSUE_KIND_FILE_MISSING, "No XDG applications directory found."); } /* holds a filename -> component mapping */ validated_cpts = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref); /* set up XML parser */ xdt = as_xmldata_new (); as_xmldata_initialize (xdt, AS_CURRENT_FORMAT_VERSION, "C", NULL, NULL, NULL, 0); as_xmldata_set_format_style (xdt, AS_FORMAT_STYLE_METAINFO); /* validate all metainfo files */ mfiles = as_utils_find_files_matching (metainfo_dir, "*.xml", FALSE, NULL); mfiles_legacy = as_utils_find_files_matching (legacy_metainfo_dir, "*.xml", FALSE, NULL); /* in case we only have legacy files */ if (mfiles == NULL) mfiles = g_ptr_array_new_with_free_func (g_free); if (mfiles_legacy != NULL) { for (i = 0; i < mfiles_legacy->len; i++) { const gchar *fname; g_autofree gchar *fname_basename = NULL; /* process metainfo files in legacy paths */ fname = (const gchar*) g_ptr_array_index (mfiles_legacy, i); fname_basename = g_path_get_basename (fname); as_validator_set_current_fname (validator, fname_basename); as_validator_add_issue (validator, NULL, AS_ISSUE_IMPORTANCE_INFO, AS_ISSUE_KIND_LEGACY, "The metainfo file is stored in a legacy path. Please place it in '/usr/share/metainfo'."); g_ptr_array_add (mfiles, g_strdup (fname)); } } for (i = 0; i < mfiles->len; i++) { const gchar *fname; g_autoptr(GFile) file = NULL; g_autoptr(GInputStream) file_stream = NULL; g_autoptr(GError) tmp_error = NULL; g_autoptr(GString) asdata = NULL; gssize len; const gsize buffer_size = 1024 * 24; g_autofree gchar *buffer = NULL; xmlNode *root; xmlDoc *doc; g_autofree gchar *fname_basename = NULL; fname = (const gchar*) g_ptr_array_index (mfiles, i); file = g_file_new_for_path (fname); if (!g_file_query_exists (file, NULL)) { g_warning ("File '%s' suddenly vanished.", fname); g_object_unref (file); continue; } fname_basename = g_path_get_basename (fname); as_validator_set_current_fname (validator, fname_basename); /* load a plaintext file */ file_stream = G_INPUT_STREAM (g_file_read (file, NULL, &tmp_error)); if (tmp_error != NULL) { as_validator_add_issue (validator, NULL, AS_ISSUE_IMPORTANCE_ERROR, AS_ISSUE_KIND_READ_ERROR, "Unable to read file: %s", tmp_error->message); continue; } asdata = g_string_new (""); buffer = g_malloc (buffer_size); while ((len = g_input_stream_read (file_stream, buffer, buffer_size, NULL, &tmp_error)) > 0) { g_string_append_len (asdata, buffer, len); } /* check if there was an error */ if (tmp_error != NULL) { as_validator_add_issue (validator, NULL, AS_ISSUE_IMPORTANCE_ERROR, AS_ISSUE_KIND_READ_ERROR, "Unable to read file: %s", tmp_error->message); continue; } /* now read the XML */ doc = as_validator_open_xml_document (validator, xdt, asdata->str); if (doc == NULL) { as_validator_clear_current_fname (validator); continue; } root = xmlDocGetRootElement (doc); if (g_strcmp0 ((gchar*) root->name, "component") == 0) { AsComponent *cpt; cpt = as_validator_validate_component_node (validator, xdt, root); if (cpt != NULL) g_hash_table_insert (validated_cpts, g_strdup (fname_basename), cpt); } else if (g_strcmp0 ((gchar*) root->name, "components") == 0) { as_validator_add_issue (validator, root, AS_ISSUE_IMPORTANCE_ERROR, AS_ISSUE_KIND_TAG_NOT_ALLOWED, "The metainfo file specifies multiple components. This is not allowed."); ret = FALSE; } else if (g_str_has_prefix ((gchar*) root->name, "application")) { as_validator_add_issue (validator, root, AS_ISSUE_IMPORTANCE_ERROR, AS_ISSUE_KIND_LEGACY, "The metainfo file uses an ancient version of the AppStream specification, which can not be validated. Please migrate it to version 0.6 (or higher)."); ret = FALSE; } as_validator_clear_current_fname (validator); xmlFreeDoc (doc); } /* check if we have matching .desktop files */ dfilenames = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); dfiles = as_utils_find_files_matching (apps_dir, "*.desktop", FALSE, NULL); if (dfiles != NULL) { for (i = 0; i < dfiles->len; i++) { const gchar *fname; fname = (const gchar*) g_ptr_array_index (dfiles, i); g_hash_table_add (dfilenames, g_path_get_basename (fname)); } } /* validate the component-id <-> filename relations and availability of other metadata */ ht_helper.validator = validator; ht_helper.desktop_fnames = dfilenames; ht_helper.apps_dir = apps_dir; g_hash_table_foreach (validated_cpts, (GHFunc) as_validator_analyze_component_metainfo_relation_cb, &ht_helper); out: if (dfilenames != NULL) g_hash_table_unref (dfilenames); if (validated_cpts != NULL) g_hash_table_unref (validated_cpts); return ret; }
/** * as_validator_analyze_component_metainfo_relation_cb: * * Helper function for GHashTable foreach iteration. */ static void as_validator_analyze_component_metainfo_relation_cb (const gchar *fname, AsComponent *cpt, struct MInfoCheckData *data) { gchar *tmp; /* if we have no component-id, we can't check anything */ if (as_component_get_id (cpt) == NULL) return; as_validator_set_current_cpt (data->validator, cpt); as_validator_set_current_fname (data->validator, fname); /* check if the fname and the component-id match */ tmp = g_strndup (as_component_get_id (cpt), g_strrstr (as_component_get_id (cpt), ".") - as_component_get_id (cpt)); if (!as_matches_metainfo (fname, tmp)) { /* the name-without-type didn't match - check for the full id in the component name */ if (!as_matches_metainfo (fname, as_component_get_id (cpt))) { as_validator_add_issue (data->validator, AS_ISSUE_IMPORTANCE_WARNING, AS_ISSUE_KIND_WRONG_NAME, "The metainfo filename does not match the component ID."); } } g_free (tmp); /* check if the referenced .desktop file exists */ if (as_component_get_kind (cpt) == AS_COMPONENT_KIND_DESKTOP_APP) { if (g_hash_table_contains (data->desktop_fnames, as_component_get_id (cpt))) { g_autofree gchar *desktop_fname_full = NULL; g_autoptr(GKeyFile) dfile = NULL; GError *tmp_error = NULL; desktop_fname_full = g_build_filename (data->apps_dir, as_component_get_id (cpt), NULL); dfile = g_key_file_new (); g_key_file_load_from_file (dfile, desktop_fname_full, G_KEY_FILE_NONE, &tmp_error); if (tmp_error != NULL) { as_validator_add_issue (data->validator, AS_ISSUE_IMPORTANCE_WARNING, AS_ISSUE_KIND_READ_ERROR, "Unable to read associated .desktop file: %s", tmp_error->message); g_error_free (tmp_error); tmp_error = NULL; } else { /* we successfully opened the .desktop file, now perform some checks */ /* name */ if ((g_strcmp0 (as_component_get_name (cpt), "") == 0) && (!g_key_file_has_key (dfile, "Desktop Entry", "Name", NULL))) { /* we don't have a summary, and there is also none in the .desktop file - this is bad. */ as_validator_add_issue (data->validator, AS_ISSUE_IMPORTANCE_ERROR, AS_ISSUE_KIND_VALUE_MISSING, "The component is missing a name (none found in its metainfo or .desktop file)"); } /* summary */ if ((g_strcmp0 (as_component_get_summary (cpt), "") == 0) && (!g_key_file_has_key (dfile, "Desktop Entry", "Comment", NULL))) { /* we don't have a summary, and there is also none in the .desktop file - this is bad. */ as_validator_add_issue (data->validator, AS_ISSUE_IMPORTANCE_ERROR, AS_ISSUE_KIND_VALUE_MISSING, "The component is missing a summary (none found in its metainfo or .desktop file)"); } } } else { as_validator_add_issue (data->validator, AS_ISSUE_IMPORTANCE_ERROR, AS_ISSUE_KIND_FILE_MISSING, "Component metadata refers to a non-existing .desktop file."); } } as_validator_clear_current_cpt (data->validator); as_validator_clear_current_fname (data->validator); }