/* vim: set ts=2 sw=2 et: */ /* * Copyright (C) 2002, 2004 Red Hat, Inc. * Copyright (C) 2006-2008 Vincent Untz * * Program written by Havoc Pennington * Ray Strode * Vincent Untz * * update-desktop-database is free software; you can redistribute it * and/or modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of the * License, or (at your option) any later version. * * update-desktop-database is distributed in the hope that it will be * useful, but WITHOUT ANY WARRANTY; without even the implied warranty * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with update-desktop-database; see the file COPYING. If not, * write to the Free Software Foundation, Inc., 59 Temple Place - Suite * 330, Boston, MA 02111-1307, USA. */ #include #include #include #include #include #include #include #include #include #include "keyfileutils.h" #include "validate.h" static gboolean edit_mode = FALSE; static const char** args = NULL; static gboolean delete_original = FALSE; static gboolean rebuild_mime_info_cache = FALSE; static char *vendor_name = NULL; static char *target_dir = NULL; static GSList *edit_actions = NULL; static mode_t permissions = 0644; typedef enum { DFU_SET_KEY_BUILDING, /* temporary type to create an action in multiple steps */ DFU_SET_KEY, DFU_REMOVE_KEY, DFU_ADD_TO_LIST, DFU_REMOVE_FROM_LIST, DFU_COPY_KEY } DfuEditActionType; typedef struct { DfuEditActionType type; char *key; char *action_value; } DfuEditAction; static DfuEditAction * dfu_edit_action_new (DfuEditActionType type, const char *key, const char *action_value) { DfuEditAction *action; action = g_slice_new0 (DfuEditAction); action->type = type; action->key = g_strdup (key); action->action_value = g_strdup (action_value); return action; } static void dfu_edit_action_free (DfuEditAction *action) { g_assert (action != NULL); g_free (action->key); g_free (action->action_value); g_slice_free (DfuEditAction, action); } static gboolean files_are_the_same (const char *first, const char *second) { /* This check gets confused by hard links. * but it's too annoying to check if two * paths are the same (though I'm sure there's a * "path canonicalizer" I should be using...) */ struct stat first_sb; struct stat second_sb; if (stat (first, &first_sb) < 0) { g_printerr (_("Could not stat \"%s\": %s\n"), first, g_strerror (errno)); return TRUE; } if (stat (second, &second_sb) < 0) { g_printerr (_("Could not stat \"%s\": %s\n"), first, g_strerror (errno)); return TRUE; } return ((first_sb.st_dev == second_sb.st_dev) && (first_sb.st_ino == second_sb.st_ino) && /* Broken paranoia in case an OS doesn't use inodes or something */ (first_sb.st_size == second_sb.st_size) && (first_sb.st_mtime == second_sb.st_mtime)); } static gboolean rebuild_cache (const char *dir, GError **err) { GError *spawn_error; char *argv[4] = { "update-desktop-database", "-q", (char *) dir, NULL }; int exit_status; gboolean retval; spawn_error = NULL; retval = g_spawn_sync (NULL, argv, NULL, G_SPAWN_SEARCH_PATH, NULL, NULL, NULL, NULL, &exit_status, &spawn_error); if (spawn_error != NULL) { g_propagate_error (err, spawn_error); return FALSE; } return exit_status == 0 && retval; } static void process_one_file (const char *filename, GError **err) { char *new_filename; GKeyFile *kf = NULL; GError *rebuild_error; GSList *tmp; kf = g_key_file_new (); if (!g_key_file_load_from_file (kf, filename, G_KEY_FILE_KEEP_COMMENTS| G_KEY_FILE_KEEP_TRANSLATIONS, err)) { g_key_file_free (kf); return; } if (!desktop_file_fixup (kf, filename)) { g_key_file_free (kf); g_set_error (err, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_PARSE, _("Failed to fix the content of the desktop file")); return; } /* Mark file as having been processed by us, so automated * tools can check that desktop files went through our * munging */ g_key_file_set_string (kf, GROUP_DESKTOP_ENTRY, "X-Desktop-File-Install-Version", VERSION); tmp = edit_actions; while (tmp != NULL) { DfuEditAction *action = tmp->data; switch (action->type) { case DFU_SET_KEY: g_key_file_set_string (kf, GROUP_DESKTOP_ENTRY, action->key, action->action_value); dfu_key_file_drop_locale_strings (kf, GROUP_DESKTOP_ENTRY, action->key); break; case DFU_REMOVE_KEY: g_key_file_remove_key (kf, GROUP_DESKTOP_ENTRY, action->key, NULL); dfu_key_file_drop_locale_strings (kf, GROUP_DESKTOP_ENTRY, action->key); break; case DFU_ADD_TO_LIST: dfu_key_file_merge_list (kf, GROUP_DESKTOP_ENTRY, action->key, action->action_value); break; case DFU_REMOVE_FROM_LIST: dfu_key_file_remove_list (kf, GROUP_DESKTOP_ENTRY, action->key, action->action_value); break; case DFU_COPY_KEY: dfu_key_file_copy_key (kf, GROUP_DESKTOP_ENTRY, action->key, GROUP_DESKTOP_ENTRY, action->action_value); break; default: g_assert_not_reached (); } tmp = tmp->next; } if (edit_mode) { new_filename = g_strdup (filename); } else { char *basename = g_path_get_basename (filename); if (vendor_name && !g_str_has_prefix (basename, vendor_name)) { char *new_base; new_base = g_strconcat (vendor_name, "-", basename, NULL); new_filename = g_build_filename (target_dir, new_base, NULL); g_free (new_base); } else { new_filename = g_build_filename (target_dir, basename, NULL); } g_free (basename); } if (!dfu_key_file_to_path (kf, new_filename, err)) { g_key_file_free (kf); g_free (new_filename); return; } g_key_file_free (kf); /* Load and validate the file we just wrote */ if (!desktop_file_validate (new_filename, FALSE, TRUE, TRUE)) { g_set_error (err, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_PARSE, _("Failed to validate the created desktop file")); if (!files_are_the_same (filename, new_filename)) g_unlink (new_filename); g_free (new_filename); return; } if (!edit_mode) { if (g_chmod (new_filename, permissions) < 0) { g_set_error (err, G_FILE_ERROR, g_file_error_from_errno (errno), _("Failed to set permissions %o on \"%s\": %s"), permissions, new_filename, g_strerror (errno)); if (!files_are_the_same (filename, new_filename)) g_unlink (new_filename); g_free (new_filename); return; } if (delete_original && !files_are_the_same (filename, new_filename)) { if (g_unlink (filename) < 0) g_printerr (_("Error removing original file \"%s\": %s\n"), filename, g_strerror (errno)); } } if (rebuild_mime_info_cache) { rebuild_error = NULL; rebuild_cache (target_dir, &rebuild_error); if (rebuild_error != NULL) g_propagate_error (err, rebuild_error); } g_free (new_filename); } static gboolean parse_install_options_callback (const gchar *option_name, const gchar *value, gpointer data, GError **error); static gboolean parse_edit_options_callback (const gchar *option_name, const gchar *value, gpointer data, GError **error); static const GOptionEntry main_options[] = { { "rebuild-mime-info-cache", '\0', '\0', G_OPTION_ARG_NONE, &rebuild_mime_info_cache, N_("Rebuild the MIME types application database after processing desktop files"), NULL }, { "edit-mode", '\0', G_OPTION_FLAG_HIDDEN, /* just for development purpose */ G_OPTION_ARG_NONE, &edit_mode, N_("Force edit mode"), NULL }, { G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &args, NULL, N_("[FILE...]") }, { NULL } }; static const GOptionEntry install_options[] = { { #define OPTION_DIR "dir" OPTION_DIR, '\0', '\0', G_OPTION_ARG_CALLBACK, parse_install_options_callback, N_("Install desktop files to the DIR directory"), N_("DIR") }, { #define OPTION_MODE "mode" OPTION_MODE, 'm', '\0', G_OPTION_ARG_CALLBACK, parse_install_options_callback, N_("Set the permissions of the destination files to MODE"), N_("MODE") }, { #define OPTION_VENDOR "vendor" OPTION_VENDOR, '\0', '\0', G_OPTION_ARG_CALLBACK, parse_install_options_callback, N_("Add a vendor prefix to the desktop files, if not already present"), N_("VENDOR") }, { "delete-original", '\0', '\0', G_OPTION_ARG_NONE, &delete_original, N_("Delete the source desktop files, leaving only the target files (effectively \"renames\" the desktop files)"), NULL }, { NULL } }; static const GOptionEntry edit_options[] = { { #define OPTION_SET_KEY "set-key" OPTION_SET_KEY, '\0', '\0', G_OPTION_ARG_CALLBACK, parse_edit_options_callback, N_("Set the KEY key to VALUE passed to next --set-value option"), N_("KEY") }, { #define OPTION_SET_VALUE "set-value" OPTION_SET_VALUE, '\0', '\0', G_OPTION_ARG_CALLBACK, parse_edit_options_callback, N_("Set the KEY key from previous --set-key option to VALUE"), N_("VALUE") }, { #define OPTION_SET_NAME "set-name" OPTION_SET_NAME, '\0', '\0', G_OPTION_ARG_CALLBACK, parse_edit_options_callback, N_("Set the \"Name\" key to NAME"), N_("NAME") }, { #define OPTION_COPY_GENERIC_NAME "copy-generic-name-to-name" OPTION_COPY_GENERIC_NAME, '\0', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, parse_edit_options_callback, N_("Copy the value of the \"GenericName\" key to the \"Name\" key"), NULL }, { #define OPTION_SET_GENERIC_NAME "set-generic-name" OPTION_SET_GENERIC_NAME, '\0', '\0', G_OPTION_ARG_CALLBACK, parse_edit_options_callback, N_("Set the \"GenericName\" key to GENERIC-NAME"), N_("GENERIC-NAME") }, { #define OPTION_COPY_NAME "copy-name-to-generic-name" OPTION_COPY_NAME, '\0', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, parse_edit_options_callback, N_("Copy the value of the \"Name\" key to the \"GenericName\" key"), NULL }, { #define OPTION_SET_COMMENT "set-comment" OPTION_SET_COMMENT, '\0', '\0', G_OPTION_ARG_CALLBACK, parse_edit_options_callback, N_("Set the \"Comment\" key to COMMENT"), N_("COMMENT") }, { #define OPTION_SET_ICON "set-icon" OPTION_SET_ICON, '\0', '\0', G_OPTION_ARG_CALLBACK, parse_edit_options_callback, N_("Set the \"Icon\" key to ICON"), N_("ICON") }, { #define OPTION_ADD_CATEGORY "add-category" OPTION_ADD_CATEGORY, '\0', '\0', G_OPTION_ARG_CALLBACK, parse_edit_options_callback, N_("Add CATEGORY to the list of categories"), N_("CATEGORY") }, { #define OPTION_REMOVE_CATEGORY "remove-category" OPTION_REMOVE_CATEGORY, '\0', '\0', G_OPTION_ARG_CALLBACK, parse_edit_options_callback, N_("Remove CATEGORY from the list of categories"), N_("CATEGORY") }, { #define OPTION_ADD_MIME_TYPE "add-mime-type" OPTION_ADD_MIME_TYPE, '\0', '\0', G_OPTION_ARG_CALLBACK, parse_edit_options_callback, N_("Add MIME-TYPE to the list of MIME types"), N_("MIME-TYPE") }, { #define OPTION_REMOVE_MIME_TYPE "remove-mime-type" OPTION_REMOVE_MIME_TYPE, '\0', '\0', G_OPTION_ARG_CALLBACK, parse_edit_options_callback, N_("Remove MIME-TYPE from the list of MIME types"), N_("MIME-TYPE") }, { #define OPTION_ADD_ONLY_SHOW_IN "add-only-show-in" OPTION_ADD_ONLY_SHOW_IN, '\0', '\0', G_OPTION_ARG_CALLBACK, parse_edit_options_callback, N_("Add ENVIRONMENT to the list of desktop environment where the desktop files should be displayed"), N_("ENVIRONMENT") }, { #define OPTION_REMOVE_ONLY_SHOW_IN "remove-only-show-in" OPTION_REMOVE_ONLY_SHOW_IN, '\0', '\0', G_OPTION_ARG_CALLBACK, parse_edit_options_callback, N_("Remove ENVIRONMENT from the list of desktop environment where the desktop files should be displayed"), N_("ENVIRONMENT") }, { #define OPTION_ADD_NOT_SHOW_IN "add-not-show-in" OPTION_ADD_NOT_SHOW_IN, '\0', '\0', G_OPTION_ARG_CALLBACK, parse_edit_options_callback, N_("Add ENVIRONMENT to the list of desktop environment where the desktop files should not be displayed"), N_("ENVIRONMENT") }, { #define OPTION_REMOVE_NOT_SHOW_IN "remove-not-show-in" OPTION_REMOVE_NOT_SHOW_IN, '\0', '\0', G_OPTION_ARG_CALLBACK, parse_edit_options_callback, N_("Remove ENVIRONMENT from the list of desktop environment where the desktop files should not be displayed"), N_("ENVIRONMENT") }, { #define OPTION_REMOVE_KEY "remove-key" OPTION_REMOVE_KEY, '\0', '\0', G_OPTION_ARG_CALLBACK, parse_edit_options_callback, N_("Remove the KEY key from the desktop files, if present"), N_("KEY") }, { NULL } }; static gboolean parse_install_options_callback (const gchar *option_name, const gchar *value, gpointer data, GError **error) { /* skip "-" or "--" */ option_name++; if (*option_name == '-') option_name++; if (strcmp (OPTION_DIR, option_name) == 0) { if (target_dir) { g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_FAILED, _("Can only specify --dir once")); return FALSE; } target_dir = g_strdup (value); } else if (strcmp (OPTION_MODE, option_name) == 0 || strcmp ("m", option_name) == 0) { unsigned long ul; char *end; end = NULL; ul = strtoul (value, &end, 8); if (*value && end && *end == '\0') permissions = ul; else { g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_FAILED, _("Could not parse mode string \"%s\""), value); return FALSE; } } else if (strcmp (OPTION_VENDOR, option_name) == 0) { if (vendor_name) { g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_FAILED, _("Can only specify --vendor once")); return FALSE; } vendor_name = g_strdup (value); } else { g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_FAILED, _("Unknown option \"%s\""), option_name); return FALSE; } return TRUE; } static gboolean parse_edit_options_callback (const gchar *option_name, const gchar *value, gpointer data, GError **error) { /* Note: we prepend actions to the list, so we'll need to reverse it later */ DfuEditAction *action; char **list; int i; /* skip "-" or "--" */ option_name++; if (*option_name == '-') option_name++; #define PARSE_OPTION_LIST(type, key) \ do { \ list = g_strsplit (value, ";", 0); \ for (i = 0; list[i]; i++) \ { \ if (*list[i] == '\0') \ continue; \ \ action = dfu_edit_action_new (type, key, list[i]); \ edit_actions = g_slist_prepend (edit_actions, action); \ } \ } while (0) if (strcmp (OPTION_SET_KEY, option_name) == 0) { action = dfu_edit_action_new (DFU_SET_KEY_BUILDING, value, NULL); edit_actions = g_slist_prepend (edit_actions, action); } else if (strcmp (OPTION_SET_VALUE, option_name) == 0) { gboolean handled = FALSE; if (edit_actions != NULL) { action = edit_actions->data; if (action->type == DFU_SET_KEY_BUILDING) { action->type = DFU_SET_KEY; action->action_value = g_strdup (value); handled = TRUE; } } if (!handled) { g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_FAILED, _("Option \"--%s\" used without a prior \"--%s\" option"), OPTION_SET_VALUE, OPTION_SET_KEY); return FALSE; } } else if (strcmp (OPTION_SET_NAME, option_name) == 0) { action = dfu_edit_action_new (DFU_SET_KEY, "Name", value); edit_actions = g_slist_prepend (edit_actions, action); } else if (strcmp (OPTION_COPY_GENERIC_NAME, option_name) == 0) { action = dfu_edit_action_new (DFU_COPY_KEY, "GenericName", "Name"); edit_actions = g_slist_prepend (edit_actions, action); } else if (strcmp (OPTION_SET_GENERIC_NAME, option_name) == 0) { action = dfu_edit_action_new (DFU_SET_KEY, "GenericName", value); edit_actions = g_slist_prepend (edit_actions, action); } else if (strcmp (OPTION_COPY_NAME, option_name) == 0) { action = dfu_edit_action_new (DFU_COPY_KEY, "Name", "GenericName"); edit_actions = g_slist_prepend (edit_actions, action); } else if (strcmp (OPTION_SET_COMMENT, option_name) == 0) { action = dfu_edit_action_new (DFU_SET_KEY, "Comment", value); edit_actions = g_slist_prepend (edit_actions, action); } else if (strcmp (OPTION_SET_ICON, option_name) == 0) { action = dfu_edit_action_new (DFU_SET_KEY, "Icon", value); edit_actions = g_slist_prepend (edit_actions, action); } else if (strcmp (OPTION_ADD_CATEGORY, option_name) == 0) { PARSE_OPTION_LIST (DFU_ADD_TO_LIST, "Categories"); } else if (strcmp (OPTION_REMOVE_CATEGORY, option_name) == 0) { PARSE_OPTION_LIST (DFU_REMOVE_FROM_LIST, "Categories"); } else if (strcmp (OPTION_ADD_MIME_TYPE, option_name) == 0) { PARSE_OPTION_LIST (DFU_ADD_TO_LIST, "MimeType"); } else if (strcmp (OPTION_REMOVE_MIME_TYPE, option_name) == 0) { PARSE_OPTION_LIST (DFU_REMOVE_FROM_LIST, "MimeType"); } else if (strcmp (OPTION_ADD_ONLY_SHOW_IN, option_name) == 0) { PARSE_OPTION_LIST (DFU_ADD_TO_LIST, "OnlyShowIn"); } else if (strcmp (OPTION_REMOVE_ONLY_SHOW_IN, option_name) == 0) { PARSE_OPTION_LIST (DFU_REMOVE_FROM_LIST, "OnlyShowIn"); } else if (strcmp (OPTION_ADD_NOT_SHOW_IN, option_name) == 0) { PARSE_OPTION_LIST (DFU_ADD_TO_LIST, "NotShowIn"); } else if (strcmp (OPTION_REMOVE_NOT_SHOW_IN, option_name) == 0) { PARSE_OPTION_LIST (DFU_REMOVE_FROM_LIST, "NotShowIn"); } else if (strcmp (OPTION_REMOVE_KEY, option_name) == 0) { action = dfu_edit_action_new (DFU_REMOVE_KEY, value, NULL); edit_actions = g_slist_prepend (edit_actions, action); } else { g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_FAILED, _("Unknown option \"%s\""), option_name); return FALSE; } return TRUE; } static gboolean check_no_building_in_edit_actions (GError **error) { GSList *tmp = edit_actions; while (tmp != NULL) { /* If we have an action that is BUILDING, then it means that we had a * --set-key not followed by a --set-value. This can happen when: * + the very last argument was --set-key * + --set-key was followed by another editing option * In both cases, that's bad as what we want is a --set-value following * each --set-key. */ DfuEditAction *action = tmp->data; if (action->type == DFU_SET_KEY_BUILDING) { g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_FAILED, _("Option \"--%s\" used without a following \"--%s\" option"), OPTION_SET_KEY, OPTION_SET_VALUE); return FALSE; } tmp = tmp->next; } return TRUE; } static gboolean post_parse_edit_options_callback (GOptionContext *context, GOptionGroup *group, gpointer data, GError **error) { if (!check_no_building_in_edit_actions (error)) return FALSE; /* Reverse list we created by prepending elements */ edit_actions = g_slist_reverse (edit_actions); return TRUE; } int main (int argc, char **argv) { GOptionContext *context; GOptionGroup *group; GError* err = NULL; int i; int args_len; mode_t dir_permissions; char *basename; setlocale (LC_ALL, ""); basename = g_path_get_basename (argv[0]); if (g_strcmp0 (basename, "desktop-file-edit") == 0) edit_mode = TRUE; g_free (basename); context = g_option_context_new (""); g_option_context_set_summary (context, edit_mode ? _("Edit a desktop file.") : _("Install desktop files.")); g_option_context_add_main_entries (context, main_options, NULL); if (!edit_mode) { group = g_option_group_new ("install", _("Installation options for desktop file"), _("Show desktop file installation options"), NULL, NULL); g_option_group_add_entries (group, install_options); g_option_context_add_group (context, group); } group = g_option_group_new ("edit", _("Edition options for desktop file"), _("Show desktop file edition options"), NULL, NULL); g_option_group_add_entries (group, edit_options); g_option_group_set_parse_hooks (group, NULL, post_parse_edit_options_callback); g_option_context_add_group (context, group); err = NULL; g_option_context_parse (context, &argc, &argv, &err); if (err != NULL) { g_printerr ("%s\n", err->message); g_printerr (_("Run '%s --help' to see a full list of available command line options.\n"), argv[0]); g_error_free (err); return 1; } if (!edit_mode) { if (vendor_name == NULL && g_getenv ("DESKTOP_FILE_VENDOR")) vendor_name = g_strdup (g_getenv ("DESKTOP_FILE_VENDOR")); if (target_dir == NULL && g_getenv ("DESKTOP_FILE_INSTALL_DIR")) target_dir = g_strdup (g_getenv ("DESKTOP_FILE_INSTALL_DIR")); if (target_dir == NULL) { if (g_getenv ("RPM_BUILD_ROOT")) target_dir = g_build_filename (g_getenv ("RPM_BUILD_ROOT"), DATADIR, "applications", NULL); else target_dir = g_build_filename (DATADIR, "applications", NULL); } /* Create the target directory */ dir_permissions = permissions; /* Add search bit when the target file is readable */ if (permissions & 0400) dir_permissions |= 0100; if (permissions & 0040) dir_permissions |= 0010; if (permissions & 0004) dir_permissions |= 0001; g_mkdir_with_parents (target_dir, dir_permissions); } args_len = 0; for (i = 0; args && args[i]; i++) args_len++; if (edit_mode) { if (args_len == 0) { g_printerr (_("Must specify a desktop file to process.\n")); return 1; } if (args_len > 1) { g_printerr (_("Only one desktop file can be processed at once.\n")); return 1; } } else { if (args_len == 0) { g_printerr (_("Must specify one or more desktop files to process.\n")); return 1; } } for (i = 0; args && args[i]; i++) { err = NULL; process_one_file (args[i], &err); if (err != NULL) { g_printerr (_("Error on file \"%s\": %s\n"), args[i], err->message); g_error_free (err); return 1; } } #if GLIB_CHECK_VERSION(2,28,0) g_slist_free_full (edit_actions, (GDestroyNotify) dfu_edit_action_free); #else g_slist_foreach (edit_actions, (GFunc) dfu_edit_action_free, NULL); g_slist_free (edit_actions); #endif g_option_context_free (context); return 0; }