diff options
-rw-r--r-- | ChangeLog | 49 | ||||
-rw-r--r-- | src/validate.c | 666 | ||||
-rw-r--r-- | src/validator.c | 2 |
3 files changed, 539 insertions, 178 deletions
@@ -1,3 +1,52 @@ +2007-06-04 Vincent Untz <vuntz@gnome.org> + + Don't use GKeyFile in the validator, so we really control everything. + + * src/validate.c: remove some FIXME/TODO + (validate_string_key): use g_ascii_iscntrl() instead of + !g_ascii_isprint(), small update for the current group + (validate_localestring_key): small update for the current group, don't + use GKeyFile + (validate_boolean_key): small update for the current group + (validate_numeric_key): ditto + (validate_string_regexp_list_key): use g_ascii_iscntrl() instead of + !g_ascii_isprint(), small update for the current group + (handle_type_key): small update for the current group + (handle_version_key): ditto + (handle_show_in_key): ditto + (handle_exec_key): ditto + (handle_path_key): ditto + (handle_mime_key): ditto + (handle_categories_key): small update for the current group, don't + use GKeyFile + (handle_actions_key): ditto + (handle_dev_key): ditto + (handle_mountpoint_key): ditto + (handle_encoding_key): ditto + (validate_desktop_key): ditto, the value is an argument now + (validate_keys_for_current_group): renamed from + validate_keys_for_group(), small update for the current group, don't + use GKeyFile and build a hashtable of all the keys in the current + group, also don't validate the key for Desktop Entry groups if the + name of the key couldn't be validated since this means we'll get + another error + (validate_group_name): use g_ascii_iscntrl() instead of + !g_ascii_isprint() + (validate_groups_and_keys): killed + (validate_required_keys): don't use GKeyFile + (validate_line_is_comment): new + (validate_line_looks_like_group): new + (validate_line_looks_like_entry): new + (validate_parse_line): new + (validate_parse_data): new (inspired from gkeyfile.c) + (validate_flush_parse_buffer): new (inspired from gkeyfile.c) + (validate_parse_from_fd): new (inspired from gkeyfile.c) + (validate_load_and_parse): new (inspired from gkeyfile.c) + (groups_hashtable_free): new + (desktop_file_validate): updated + (desktop_file_fixup): small update to avoid confusion + * src/validator.c: (main): fix leak + 2007-03-15 Vincent Untz <vuntz@gnome.org> * README: remove mention of desktop-menu-tool diff --git a/src/validate.c b/src/validate.c index 173c77f..e07c8cf 100644 --- a/src/validate.c +++ b/src/validate.c @@ -8,6 +8,11 @@ * Havoc Pennington * Ray Strode * + * A portion of this code comes from glib (gkeyfile.c) + * Authors of gkeyfile.c are: + * Ray Strode + * Matthias Clasen + * * This program 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 @@ -24,33 +29,27 @@ * USA. */ +#include <errno.h> +#include <fcntl.h> #include <stdio.h> #include <string.h> +#include <sys/stat.h> +#include <unistd.h> + +#include <glib.h> +#include <glib/gstdio.h> #include "keyfileutils.h" #include "validate.h" -/* We're trying to not use GKeyFile APIs as much as possible, so we don't - * have to "trust" what GKeyFile is doing, and GKeyFile can be less - * strict than this validator. - * FIXME: maybe it's a good idea to just not use GKeyFile and do all the - * parsing ourselves. This way, we know we're doing exactly what we want. - */ - -//FIXME: document where GKeyFile is stricter than the spec +//FIXME: document where we are stricter than the spec // * only UTF-8 (so no Legacy-Mixed encoding) -// * "Historically lists have been comma separated." -// Add this information to --usage /*TODO: - * + Desktop entry files are encoded as lines of 8-bit characters separated by - * LF characters. - * + Multiple groups may not have the same name. * + Lecagy-Mixed Encoding (annexe D) * + The escape sequences \s, \n, \t, \r, and \\ are supported for values of * type string and localestring, meaning ASCII space, newline, tab, carriage * return, and backslash, respectively. - * GKeyFile handles this, but we don't. */ typedef enum { @@ -85,11 +84,25 @@ typedef enum { DESKTOP_REGEXP_LIST_TYPE } DesktopKeyType; +typedef struct _kf_keyvalue kf_keyvalue; + +struct _kf_keyvalue { + char *key; + char *value; +}; + typedef struct _kf_validator kf_validator; struct _kf_validator { const char *filename; - GKeyFile *keyfile; + + GString *parse_buffer; + gboolean utf8_warning; + gboolean cr_error; + + char *current_group; + GHashTable *groups; + GHashTable *current_keys; gboolean kde_reserved_warnings; gboolean no_deprecated_warnings; @@ -401,7 +414,7 @@ validate_string_key (kf_validator *kf, error = FALSE; for (i = 0; value[i] != '\0'; i++) { - if (!g_ascii_isprint (value[i])) { + if (g_ascii_iscntrl (value[i])) { error = TRUE; break; } @@ -411,7 +424,7 @@ validate_string_key (kf_validator *kf, print_fatal (kf, "value \"%s\" for string key \"%s\" in group \"%s\" " "contains invalid characters, string values may contain " "all ASCII characters except for control characters\n", - value, key, kf->main_group); + value, key, kf->current_group); return FALSE; } @@ -443,16 +456,16 @@ validate_localestring_key (kf_validator *kf, print_fatal (kf, "value \"%s\" for locale string key \"%s\" in group " "\"%s\" contains invalid UTF-8 characters, locale string " "values should be encoded in UTF-8\n", - value, locale_key, kf->main_group); + value, locale_key, kf->current_group); g_free (locale_key); return FALSE; } - if (!g_key_file_has_key (kf->keyfile, kf->main_group, key, NULL)) { + if (!g_hash_table_lookup (kf->current_keys, key)) { print_fatal (kf, "key \"%s\" in group \"%s\" is a localized key, but " "there is no non-localized key \"%s\"\n", - locale_key, kf->main_group, key); + locale_key, kf->current_group, key); g_free (locale_key); return FALSE; @@ -483,7 +496,7 @@ validate_boolean_key (kf_validator *kf, print_fatal (kf, "value \"%s\" for boolean key \"%s\" in group \"%s\" " "contains invalid characters, boolean values must be " "\"false\" or \"true\"\n", - value, key, kf->main_group); + value, key, kf->current_group); return FALSE; } @@ -492,7 +505,7 @@ validate_boolean_key (kf_validator *kf, print_warning (kf, "boolean key \"%s\" in group \"%s\" has value \"%s\", " "which is deprecated: boolean values should be " "\"false\" or \"true\"\n", - key, kf->main_group, value); + key, kf->current_group, value); return TRUE; } @@ -515,7 +528,7 @@ validate_numeric_key (kf_validator *kf, print_fatal (kf, "value \"%s\" for numeric key \"%s\" in group \"%s\" " "contains invalid characters, numeric values must be " "valid floating point numbers\n", - value, key, kf->main_group); + value, key, kf->current_group); return FALSE; } @@ -543,7 +556,7 @@ validate_string_regexp_list_key (kf_validator *kf, error = FALSE; for (i = 0; value[i] != '\0'; i++) { - if (!g_ascii_isprint (value[i])) { + if (g_ascii_iscntrl (value[i])) { error = TRUE; break; } @@ -554,7 +567,7 @@ validate_string_regexp_list_key (kf_validator *kf, "contains invalid character '%c', %s list values may " "contain all ASCII characters except for control " "characters\n", - value, type, key, kf->main_group, value[i], type); + value, type, key, kf->current_group, value[i], type); return FALSE; } @@ -563,7 +576,7 @@ validate_string_regexp_list_key (kf_validator *kf, print_fatal (kf, "value \"%s\" for %s list key \"%s\" in group \"%s\" " "does not have a semicolon (';') as trailing " "character\n", - value, type, key, kf->main_group); + value, type, key, kf->current_group); return FALSE; } @@ -572,7 +585,7 @@ validate_string_regexp_list_key (kf_validator *kf, (i < 3 || value[i - 3] != '\\')) { print_fatal (kf, "value \"%s\" for %s list key \"%s\" in group \"%s\" " "has an escaped semicolon (';') as trailing character\n", - value, type, key, kf->main_group); + value, type, key, kf->current_group); return FALSE; } @@ -625,19 +638,19 @@ handle_type_key (kf_validator *kf, print_fatal (kf, "value \"%s\" for key \"%s\" in group \"%s\" " "is not a registered type value (\"Application\", " "\"Link\" and \"Directory\")\n", - value, locale_key, kf->main_group); + value, locale_key, kf->current_group); return FALSE; } if (registered_types[i].kde_reserved && kf->kde_reserved_warnings) print_warning (kf, "value \"%s\" for key \"%s\" in group \"%s\" " "is a reserved value for KDE\n", - value, locale_key, kf->main_group); + value, locale_key, kf->current_group); if (registered_types[i].deprecated && !kf->no_deprecated_warnings) print_warning (kf, "value \"%s\" for key \"%s\" in group \"%s\" " "is deprecated\n", - value, locale_key, kf->main_group); + value, locale_key, kf->current_group); kf->type = registered_types[i].type; kf->type_string = registered_types[i].name; @@ -669,7 +682,7 @@ handle_version_key (kf_validator *kf, print_fatal (kf, "value \"%s\" for key \"%s\" in group \"%s\" " "is not a known version\n", - value, locale_key, kf->main_group); + value, locale_key, kf->current_group); return FALSE; } @@ -692,6 +705,7 @@ handle_comment_key (kf_validator *kf, * Checked. * FIXME: this is not perfect because it could fail if a new value with * a semicolon is registered. + * + FIXME: is this okay to have only ";"? (gnome-theme-installer.desktop does) */ static gboolean handle_show_in_key (kf_validator *kf, @@ -709,7 +723,7 @@ handle_show_in_key (kf_validator *kf, if (kf->show_in) { print_fatal (kf, "only one of \"OnlyShowIn\" and \"NotShowInkey\" keys " "may appear in group \"%s\"\n", - kf->main_group); + kf->current_group); retval = FALSE; } kf->show_in = TRUE; @@ -726,7 +740,7 @@ handle_show_in_key (kf_validator *kf, if (g_hash_table_lookup (hashtable, show[i])) { print_warning (kf, "value \"%s\" for key \"%s\" in group \"%s\" " "contains \"%s\" more than once\n", - value, locale_key, kf->main_group, show[i]); + value, locale_key, kf->current_group, show[i]); continue; } @@ -740,7 +754,7 @@ handle_show_in_key (kf_validator *kf, if (j == G_N_ELEMENTS (show_in_registered)) { print_fatal (kf, "value \"%s\" for key \"%s\" in group \"%s\" " "contains an unregistered value \"%s\"\n", - value, locale_key, kf->main_group, show[i]); + value, locale_key, kf->current_group, show[i]); retval = FALSE; } } @@ -825,7 +839,7 @@ handle_exec_key (kf_validator *kf, if (flag) { \ print_fatal (kf, "value \"%s\" for key \"%s\" in group \"%s\" " \ "contains an invalid field code \"%%%c\"\n", \ - value, locale_key, kf->main_group, *c); \ + value, locale_key, kf->current_group, *c); \ retval = FALSE; \ flag = FALSE; \ break; \ @@ -848,7 +862,7 @@ handle_exec_key (kf_validator *kf, "contains an escaped double quote (\\\\\") " "outside of a quote, but the double quote is " "a reserved character\n", - value, locale_key, kf->main_group); + value, locale_key, kf->current_group); retval = FALSE; escaped = FALSE; @@ -864,7 +878,7 @@ handle_exec_key (kf_validator *kf, "contains a non-escaped character '%c' in a " "quote, but it should be escaped with two " "backslashes (\"\\\\%c\")\n", - value, locale_key, kf->main_group, *c, *c); + value, locale_key, kf->current_group, *c, *c); retval = FALSE; } else escaped = FALSE; @@ -872,7 +886,7 @@ handle_exec_key (kf_validator *kf, print_fatal (kf, "value \"%s\" for key \"%s\" in group \"%s\" " "contains a reserved character '%c' outside of a " "quote\n", - value, locale_key, kf->main_group, *c); + value, locale_key, kf->current_group, *c); retval = FALSE; } break; @@ -906,7 +920,7 @@ handle_exec_key (kf_validator *kf, print_fatal (kf, "value \"%s\" for key \"%s\" in group \"%s\" " "contains a reserved character '%c' outside of a " "quote\n", - value, locale_key, kf->main_group, *c); + value, locale_key, kf->current_group, *c); retval = FALSE; } break; @@ -922,7 +936,7 @@ handle_exec_key (kf_validator *kf, print_fatal (kf, "value \"%s\" for key \"%s\" in group \"%s\" " "may contain at most one \"%f\", \"%u\", " "\"%F\" or \"%U\" field code\n", - value, locale_key, kf->main_group); + value, locale_key, kf->current_group); retval = FALSE; } @@ -937,7 +951,7 @@ handle_exec_key (kf_validator *kf, print_fatal (kf, "value \"%s\" for key \"%s\" in group \"%s\" " "may contain at most one \"%f\", \"%u\", " "\"%F\" or \"%U\" field code\n", - value, locale_key, kf->main_group); + value, locale_key, kf->current_group); retval = FALSE; } @@ -961,7 +975,7 @@ handle_exec_key (kf_validator *kf, if (!kf->no_deprecated_warnings) print_warning (kf, "value \"%s\" for key \"%s\" in group \"%s\" " "contains a deprecated field code \"%%%c\"\n", - value, locale_key, kf->main_group, *c); + value, locale_key, kf->current_group, *c); flag = FALSE; } break; @@ -977,14 +991,14 @@ handle_exec_key (kf_validator *kf, if (in_quote) { print_fatal (kf, "value \"%s\" for key \"%s\" in group \"%s\" contains a " "quote which is not closed\n", - value, locale_key, kf->main_group); + value, locale_key, kf->current_group); retval = FALSE; } if (flag) { print_fatal (kf, "value \"%s\" for key \"%s\" in group \"%s\" contains a " "non-complete field code\n", - value, locale_key, kf->main_group); + value, locale_key, kf->current_group); retval = FALSE; } @@ -994,6 +1008,7 @@ handle_exec_key (kf_validator *kf, /* + If entry is of type Application, the working directory to run the program * in. (probably implies an absolute path) * Checked. + * + FIXME: is it okay to have an empty string here? (wireshark.desktop does) */ static gboolean handle_path_key (kf_validator *kf, @@ -1005,7 +1020,7 @@ handle_path_key (kf_validator *kf, if (!g_path_is_absolute (value)) print_warning (kf, "value \"%s\" for key \"%s\" in group \"%s\" " "does not look like an absolute path\n", - value, locale_key, kf->main_group); + value, locale_key, kf->current_group); return TRUE; } @@ -1043,7 +1058,7 @@ handle_mime_key (kf_validator *kf, if (g_hash_table_lookup (hashtable, types[i])) { print_warning (kf, "value \"%s\" for key \"%s\" in group \"%s\" " "contains \"%s\" more than once\n", - value, locale_key, kf->main_group, types[i]); + value, locale_key, kf->current_group, types[i]); continue; } @@ -1054,7 +1069,7 @@ handle_mime_key (kf_validator *kf, print_fatal (kf, "value \"%s\" for key \"%s\" in group \"%s\" " "contains value \"%s\" which does not look like " "a MIME type\n", - value, locale_key, kf->main_group, types[i]); + value, locale_key, kf->current_group, types[i]); retval = FALSE; } } @@ -1065,7 +1080,7 @@ handle_mime_key (kf_validator *kf, return retval; } -/* + FIXME: is there restrictions on how a category should be named? +/* + FIXME: are there restrictions on how a category should be named? * + Categories in which the entry should be shown in a menu (for possible * values see the Desktop Menu Specification). * Checked. @@ -1108,7 +1123,7 @@ handle_categories_key (kf_validator *kf, if (g_hash_table_lookup (hashtable, categories[i])) { print_warning (kf, "value \"%s\" for key \"%s\" in group \"%s\" " "contains \"%s\" more than once\n", - value, locale_key, kf->main_group, categories[i]); + value, locale_key, kf->current_group, categories[i]); continue; } @@ -1129,12 +1144,11 @@ handle_categories_key (kf_validator *kf, IF_CHECK_REGISTERED_CATEGORIES (additional_categories_registered) continue; IF_CHECK_REGISTERED_CATEGORIES (reserved_categories_registered) { - if (!g_key_file_has_key (kf->keyfile, kf->main_group, - "OnlyShowIn", NULL)) { + if (!g_hash_table_lookup (kf->current_keys, "OnlyShowIn")) { print_fatal (kf, "value \"%s\" in key \"%s\" in group \"%s\" " "is a reserved category, so a \"OnlyShowIn\" key " "must be included\n", - categories[i], locale_key, kf->main_group); + categories[i], locale_key, kf->current_group); retval = FALSE; } continue; @@ -1143,14 +1157,14 @@ handle_categories_key (kf_validator *kf, if (!kf->no_deprecated_warnings) print_warning (kf, "value \"%s\" for key \"%s\" in group \"%s\" " "contains a deprecated value \"%s\"\n", - value, locale_key, kf->main_group, + value, locale_key, kf->current_group, categories[i]); continue; } print_fatal (kf, "value \"%s\" for key \"%s\" in group \"%s\" " "contains an unregistered value \"%s\"\n", - value, locale_key, kf->main_group, categories[i]); + value, locale_key, kf->current_group, categories[i]); retval = FALSE; } @@ -1184,14 +1198,14 @@ handle_actions_key (kf_validator *kf, if (actions[i + 1] != NULL) print_warning (kf, "value \"%s\" for key \"%s\" in group \"%s\" " "contains an empty action\n", - value, locale_key, kf->main_group); + value, locale_key, kf->current_group); continue; } if (g_hash_table_lookup (kf->action_values, actions[i])) { print_warning (kf, "value \"%s\" for key \"%s\" in group \"%s\" " "contains action \"%s\" more than once\n", - value, locale_key, kf->main_group, actions[i]); + value, locale_key, kf->current_group, actions[i]); continue; } @@ -1217,7 +1231,7 @@ handle_dev_key (kf_validator *kf, if (!g_path_is_absolute (value)) print_warning (kf, "value \"%s\" for key \"%s\" in group \"%s\" " "does not look like an absolute path\n", - value, locale_key, kf->main_group); + value, locale_key, kf->current_group); return TRUE; } @@ -1236,7 +1250,7 @@ handle_mountpoint_key (kf_validator *kf, if (!g_path_is_absolute (value)) print_warning (kf, "value \"%s\" for key \"%s\" in group \"%s\" " "does not look like an absolute path\n", - value, locale_key, kf->main_group); + value, locale_key, kf->current_group); return TRUE; } @@ -1255,7 +1269,7 @@ handle_encoding_key (kf_validator *kf, print_fatal (kf, "value \"%s\" for key \"%s\" in group \"%s\" " "is not a registered encoding value (\"UTF-8\", and " "\"Legacy-Mixed\")\n", - value, locale_key, kf->main_group); + value, locale_key, kf->current_group); return FALSE; } @@ -1369,11 +1383,11 @@ static gboolean validate_desktop_key (kf_validator *kf, const char *locale_key, const char *key, - const char *locale) + const char *locale, + const char *value) { unsigned int i; unsigned int j; - char *value; if (!strncmp (key, "X-", 2)) return TRUE; @@ -1386,7 +1400,7 @@ validate_desktop_key (kf_validator *kf, locale != NULL) { print_fatal (kf, "file contains key \"%s\" in group \"%s\", " "but \"%s\" is not defined as a locale string\n", - locale_key, kf->main_group, key); + locale_key, kf->current_group, key); return FALSE; } @@ -1399,42 +1413,29 @@ validate_desktop_key (kf_validator *kf, if (!kf->no_deprecated_warnings && registered_desktop_keys[i].deprecated) print_warning (kf, "key \"%s\" in group \"%s\" is deprecated\n", - locale_key, kf->main_group); + locale_key, kf->current_group); if (registered_desktop_keys[i].kde_reserved && kf->kde_reserved_warnings) print_warning (kf, "key \"%s\" in group \"%s\" is a reserved key for " "KDE\n", - locale_key, kf->main_group); - - value = g_key_file_get_value (kf->keyfile, kf->main_group, - locale_key, NULL); + locale_key, kf->current_group); - /* this is not supposed to happen since we got the key from the list of - * existing keys */ - g_assert (value != NULL); - - if (!validate_for_type[j].validate (kf, key, locale, value)) { - g_free (value); + if (!validate_for_type[j].validate (kf, key, locale, value)) return FALSE; - } if (registered_desktop_keys[i].handle_and_validate != NULL) { if (!registered_desktop_keys[i].handle_and_validate (kf, locale_key, - value)) { - g_free (value); + value)) return FALSE; - } } - g_free (value); - break; } if (i == G_N_ELEMENTS (registered_desktop_keys)) { print_fatal (kf, "file contains key \"%s\" in group \"%s\", but " "keys extending the format should start with " - "\"X-\"\n", key, kf->main_group); + "\"X-\"\n", key, kf->current_group); return FALSE; } @@ -1445,66 +1446,100 @@ validate_desktop_key (kf_validator *kf, * Checked. */ static gboolean -validate_keys_for_group (kf_validator *kf, - const char *group) +validate_keys_for_current_group (kf_validator *kf) { gboolean desktop_group; gboolean retval; - int i; char *key; char *locale; - char **keys; - GHashTable *hashtable; + GSList *keys; + GSList *sl; retval = TRUE; - desktop_group = (!strcmp (group, GROUP_DESKTOP_ENTRY) || - !strcmp (group, GROUP_KDE_DESKTOP_ENTRY)); + desktop_group = (!strcmp (kf->current_group, GROUP_DESKTOP_ENTRY) || + !strcmp (kf->current_group, GROUP_KDE_DESKTOP_ENTRY)); - keys = g_key_file_get_keys (kf->keyfile, group, NULL, NULL); + keys = g_slist_copy (g_hash_table_lookup (kf->groups, kf->current_group)); + /* keys were prepended, so reverse the list (that's why we use a + * g_slist_copy() */ + keys = g_slist_reverse (keys); - g_assert (keys != NULL); + kf->current_keys = g_hash_table_new_full (g_str_hash, g_str_equal, + NULL, NULL); - hashtable = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, NULL); + /* we need two passes: some checks are looking if another key exists in the + * group */ + for (sl = keys; sl != NULL; sl = sl->next) { + kf_keyvalue *keyvalue; - for (i = 0; keys[i] != NULL; i++) { - if (!key_extract_locale (keys[i], &key, &locale)) { + keyvalue = (kf_keyvalue *) sl->data; + g_hash_table_insert (kf->current_keys, keyvalue->key, GINT_TO_POINTER (1)); + } + + for (sl = keys; sl != NULL; sl = sl->next) { + kf_keyvalue *keyvalue; + gboolean skip_desktop_check; + gpointer hashvalue; + + keyvalue = (kf_keyvalue *) sl->data; + + skip_desktop_check = FALSE; + + if (!key_extract_locale (keyvalue->key, &key, &locale)) { print_fatal (kf, "file contains key \"%s\" in group \"%s\", but " "key names must contain only the characters " "A-Za-z0-9- (they may have a \"[LOCALE]\" postfix)\n", - keys[i], group); + keyvalue->key, kf->current_group); retval = FALSE; + skip_desktop_check = TRUE; - key = g_strdup (keys[i]); + key = g_strdup (keyvalue->key); } g_assert (key != NULL); - if (g_hash_table_lookup (hashtable, keys[i])) { + hashvalue = g_hash_table_lookup (kf->current_keys, keyvalue->key); + if (GPOINTER_TO_INT (hashvalue) != 1) { print_fatal (kf, "file contains multiple keys named \"%s\" in " - "group \"%s\"\n", keys[i], group); + "group \"%s\"\n", keyvalue->key, kf->current_group); retval = FALSE; + } else { + g_hash_table_replace (kf->current_keys, keyvalue->key, + GINT_TO_POINTER (2)); } - if (desktop_group) { - if (!validate_desktop_key (kf, keys[i], key, locale)) + if (desktop_group && !skip_desktop_check) { + if (!validate_desktop_key (kf, keyvalue->key, + key, locale, keyvalue->value)) retval = FALSE; } - g_hash_table_insert (hashtable, keys[i], keys[i]); - g_free (key); key = NULL; g_free (locale); locale = NULL; } - g_hash_table_destroy (hashtable); - g_strfreev (keys); + g_hash_table_destroy (kf->current_keys); + kf->current_keys = NULL; return retval; } +/* + Using [KDE Desktop Entry] instead of [Desktop Entry] as header is + * deprecated. + * Checked. + * + Group names may contain all ASCII characters except for [ and ] and + * control characters. + * Checked. + * + All groups extending the format should start with "X-". + * Checked. + * + Accept "Desktop Action foobar" group if the value for the Action key + * contains "foobar". (This is not in spec 1.0, but it was there before and + * it wasn't deprecated) + * Checked. + */ static gboolean validate_group_name (kf_validator *kf, const char *group) @@ -1514,7 +1549,7 @@ validate_group_name (kf_validator *kf, for (i = 0; group[i] != '\0'; i++) { c = group[i]; - if (!g_ascii_isprint (c) || c == '[' || c == ']') { + if (g_ascii_iscntrl (c) || c == '[' || c == ']') { print_fatal (kf, "file contains group \"%s\", but group names " "may contain all ASCII characters except for [ " "and ] and control characters\n", group); @@ -1571,70 +1606,31 @@ validate_group_name (kf_validator *kf, return FALSE; } -/* + Only comments are accepted before the first group. - * FIXME: verify that GKeyFile handles this. - * + Using [KDE Desktop Entry] instead of [Desktop Entry] as header is - * deprecated. - * Checked. - * + The first group should be "Desktop Entry". - * Checked. - * + Group names may contain all ASCII characters except for [ and ] and - * control characters. - * Checked. - * + Multiple groups may not have the same name. - * FIXME: GKeyFile can't let us verify this. - * + All groups extending the format should start with "X-". - * Checked. - * + Accept "Desktop Action foobar" group if the value for the Action key - * contains "foobar". (This is not in spec 1.0, but it was there before and - * it wasn't deprecated) - * Checked. - */ static gboolean -validate_groups_and_keys (kf_validator *kf) +validate_required_keys (kf_validator *kf) { - gboolean retval; - int i; - char *group; - char **groups; + gboolean retval; + unsigned int i; + GSList *sl; + GSList *keys; + GHashTable *hashtable; retval = TRUE; - group = g_key_file_get_start_group (kf->keyfile); - if (!group || - (strcmp (group, GROUP_DESKTOP_ENTRY) && - strcmp (group, GROUP_KDE_DESKTOP_ENTRY))) - print_fatal (kf, "first group is not \"" GROUP_DESKTOP_ENTRY "\"\n"); - g_free (group); - - groups = g_key_file_get_groups (kf->keyfile, NULL); - i = 0; - for (i = 0; groups[i] != NULL; i++) { - group = groups[i]; + hashtable = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, NULL); + keys = g_hash_table_lookup (kf->groups, kf->main_group); - if (!validate_group_name (kf, group)) - retval = FALSE; + for (sl = keys; sl != NULL; sl = sl->next) { + kf_keyvalue *keyvalue; - if (!validate_keys_for_group (kf, group)) - retval = FALSE; + keyvalue = (kf_keyvalue *) sl->data; + g_hash_table_insert (hashtable, keyvalue->key, keyvalue->key); } - g_strfreev (groups); - return retval; -} - -static gboolean -validate_required_keys (kf_validator *kf) -{ - gboolean retval; - unsigned int i; - - retval = TRUE; - for (i = 0; i < G_N_ELEMENTS (registered_desktop_keys); i++) { if (registered_desktop_keys[i].required) { - if (!g_key_file_has_key (kf->keyfile, kf->main_group, - registered_desktop_keys[i].name, NULL)) { + if (!g_hash_table_lookup (hashtable, + registered_desktop_keys[i].name)) { print_fatal (kf, "required key \"%s\" in group \"%s\" is not " "present\n", registered_desktop_keys[i].name, kf->main_group); @@ -1643,6 +1639,8 @@ validate_required_keys (kf_validator *kf) } } + g_hash_table_destroy (hashtable); + return retval; } @@ -1821,34 +1819,339 @@ validate_filename (kf_validator *kf) return FALSE; } +/* + Lines beginning with a # and blank lines are considered comments. + * Checked. + */ +static gboolean +validate_line_is_comment (kf_validator *kf, + const char *line) +{ + return (*line == '#' || *line == '\0'); +} + +/* + A group header with name groupname is a line in the format: [groupname] + * Checked. + * + Group names may contain all ASCII characters except for [ and ] and + * control characters. + * This is done in validate_group_name(). + */ +static gboolean +validate_line_looks_like_group (kf_validator *kf, + const char *line, + char **group) +{ + char *chomped; + gboolean result; + + chomped = g_strdup (line); + g_strchomp (chomped); + + result = (*chomped == '[' && chomped[strlen (chomped) - 1] == ']'); + + if (result && strcmp (chomped, line)) + print_fatal (kf, "line \"%s\" ends with a space, but looks like a group. " + "The validation will continue, with the trailing spaces " + "ignored.\n", line); + + *group = g_strndup (chomped + 1, strlen (chomped) - 2); + + g_free (chomped); + + return result; +} + +/* + Space before and after the equals sign should be ignored; the = sign is + * the actual delimiter. + * Checked. + */ +static gboolean +validate_line_looks_like_entry (kf_validator *kf, + const char *line, + char **key, + char **value) +{ + char *p; + + p = g_utf8_strchr (line, -1, '='); + + if (!p) + return FALSE; + + /* key must be non-empty */ + if (*p == line[0]) + return FALSE; + + if (key) { + *key = g_strndup (line, p - line); + g_strchomp (*key); + } + if (value) { + *value = g_strdup (p + 1); + g_strchug (*value); + } + + return TRUE; +} + +/* + Only comments are accepted before the first group. + * Checked. + * + The first group should be "Desktop Entry". + * Checked. + * + Multiple groups may not have the same name. + * Checked. + */ +static void +validate_parse_line (kf_validator *kf) +{ + char *line; + int len; + char *group; + char *key; + char *value; + + line = kf->parse_buffer->str; + len = kf->parse_buffer->len; + + if (!kf->utf8_warning && !g_utf8_validate (line, len, NULL)) { + print_warning (kf, "file contains lines that are not UTF-8 encoded. There " + "is no guarantee the validator will correctly work.\n"); + kf->utf8_warning = TRUE; + } + + if (g_ascii_isspace (*line)) { + print_fatal (kf, "line \"%s\" starts with a space. Comment, group and " + "key-value lines should not start with a space. The " + "validation will continue, with the leading spaces " + "ignored.\n", line); + while (g_ascii_isspace (*line)) + line++; + } + + if (validate_line_is_comment (kf, line)) + return; + + if (validate_line_looks_like_group (kf, line, &group)) { + if (!kf->current_group && + (strcmp (group, GROUP_DESKTOP_ENTRY) && + strcmp (group, GROUP_KDE_DESKTOP_ENTRY))) + print_fatal (kf, "first group is not \"" GROUP_DESKTOP_ENTRY "\"\n"); + + if (kf->current_group && strcmp (kf->current_group, group)) + validate_keys_for_current_group (kf); + + if (g_hash_table_lookup_extended (kf->groups, group, NULL, NULL)) { + print_fatal (kf, "file contains multiple groups named \"%s\", but " + "multiple groups may not have the same name\n", group); + } else { + validate_group_name (kf, group); + g_hash_table_insert (kf->groups, g_strdup (group), NULL); + } + + if (kf->current_group) + g_free (kf->current_group); + kf->current_group = group; + + return; + } + + if (validate_line_looks_like_entry (kf, line, &key, &value)) { + if (kf->current_group) { + GSList *keys; + kf_keyvalue *keyvalue; + + keyvalue = g_slice_new (kf_keyvalue); + keyvalue->key = key; + keyvalue->value = value; + + keys = g_hash_table_lookup (kf->groups, kf->current_group); + keys = g_slist_prepend (keys, keyvalue); + g_hash_table_replace (kf->groups, g_strdup (kf->current_group), keys); + } else { + print_fatal (kf, "file contains entry \"%s\" before the first group, " + "but only comments are accepted before the first " + "group\n", line); + } + + return; + } + + print_fatal (kf, "file contains line \"%s\", which is not a comment, " + "a group or an entry\n", line); +} + +/* + Desktop entry files are encoded as lines of 8-bit characters separated by + * LF characters. + * Checked. + */ +static void +validate_parse_data (kf_validator *kf, + char *data, + int length) +{ + int i; + + for (i = 0; i < length; i++) { + if (data[i] == '\n') { + if (i > 0 && data[i - 1] == '\r') { + g_string_erase (kf->parse_buffer, kf->parse_buffer->len - 1, 1); + + if (!kf->cr_error) { + print_fatal (kf, "file contains at least one line ending with a " + "carriage return before the line feed, while lines " + "should only be separated by a line feed " + "character. First such line is: \"%s\"\n", + kf->parse_buffer->str); + kf->cr_error = TRUE; + } + } + + if (kf->parse_buffer->len > 0) { + validate_parse_line (kf); + g_string_erase (kf->parse_buffer, 0, -1); + } + + } else if (data[i] == '\r') { + if (!kf->cr_error) { + print_fatal (kf, "file contains at least one line ending with a " + "carriage return, while lines should only be " + "separated by a line feed character. First such " + "line is: \"%s\"\n", kf->parse_buffer->str); + kf->cr_error = TRUE; + } + + data[i] = '\n'; + i--; + } else + g_string_append_c (kf->parse_buffer, data[i]); + } +} + +static void +validate_flush_parse_buffer (kf_validator *kf) +{ + if (kf->parse_buffer->len > 0) { + validate_parse_line (kf); + g_string_erase (kf->parse_buffer, 0, -1); + } + + if (kf->current_group) + validate_keys_for_current_group (kf); +} + +#define VALIDATE_READ_SIZE 4096 +static gboolean +validate_parse_from_fd (kf_validator *kf, + int fd) +{ + int bytes_read; + struct stat stat_buf; + char read_buf[VALIDATE_READ_SIZE]; + + if (fstat (fd, &stat_buf) < 0) { + print_fatal (kf, "while reading the file: %s\n", g_strerror (errno)); + return FALSE; + } + + if (!S_ISREG (stat_buf.st_mode)) { + print_fatal (kf, "file is not a regular file\n"); + return FALSE; + } + + if (stat_buf.st_size == 0) { + print_fatal (kf, "file is empty\n"); + return FALSE; + } + + bytes_read = 0; + while (1) { + bytes_read = read (fd, read_buf, VALIDATE_READ_SIZE); + + if (bytes_read == 0) /* End of File */ + break; + + if (bytes_read < 0) { + if (errno == EINTR || errno == EAGAIN) + continue; + + /* let's validate what we already have */ + validate_flush_parse_buffer (kf); + + print_fatal (kf, "while reading the file: %s\n", g_strerror (errno)); + return FALSE; + } + + validate_parse_data (kf, read_buf, bytes_read); + } + + validate_flush_parse_buffer (kf); + + return TRUE; +} + +static gboolean +validate_load_and_parse (kf_validator *kf) +{ + int fd; + gboolean ret; + + fd = g_open (kf->filename, O_RDONLY, 0); + + if (fd < 0) { + print_fatal (kf, "while reading the file: %s\n", g_strerror (errno)); + return FALSE; + } + + ret = validate_parse_from_fd (kf, fd); + + close (fd); + + return ret; +} + +static gboolean +groups_hashtable_free (gpointer key, + gpointer value, + gpointer data) +{ + GSList *list; + GSList *sl; + + list = (GSList *) value; + for (sl = list; sl != NULL; sl = sl->next) { + kf_keyvalue *keyvalue; + + keyvalue = (kf_keyvalue *) sl->data; + g_free (keyvalue->key); + g_free (keyvalue->value); + g_slice_free (kf_keyvalue, keyvalue); + } + + g_slist_free (list); + + return TRUE; +} + gboolean desktop_file_validate (const char *filename, gboolean warn_kde, gboolean no_warn_deprecated) { - GError *error; - kf_validator kf; + kf_validator kf; /* just a consistency check */ g_assert (G_N_ELEMENTS (registered_types) == LAST_TYPE - 1); kf.filename = filename; - kf.keyfile = g_key_file_new (); + kf.parse_buffer = g_string_new (""); + kf.utf8_warning = FALSE; + kf.cr_error = FALSE; + kf.current_group = NULL; + kf.groups = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, NULL); + kf.current_keys = NULL; kf.kde_reserved_warnings = warn_kde; kf.no_deprecated_warnings = no_warn_deprecated; - error = NULL; - if (!g_key_file_load_from_file (kf.keyfile, filename, - G_KEY_FILE_KEEP_COMMENTS| - G_KEY_FILE_KEEP_TRANSLATIONS, - &error)) { - print_fatal (&kf, "parse error: %s\n", error->message); - g_error_free (error); - g_key_file_free (kf.keyfile); - - return FALSE; - } - kf.main_group = NULL; kf.type = INVALID_TYPE; kf.type_string = NULL; @@ -1863,7 +2166,7 @@ desktop_file_validate (const char *filename, NULL, g_free); kf.fatal_error = FALSE; - validate_groups_and_keys (&kf); + validate_load_and_parse (&kf); //FIXME: this does not work well if there are both a Desktop Entry and a KDE //Desktop Entry groups since only the last one will be validated for this. if (kf.main_group) { @@ -1885,23 +2188,30 @@ desktop_file_validate (const char *filename, g_hash_table_destroy (kf.action_values); g_hash_table_destroy (kf.action_groups); - g_key_file_free (kf.keyfile); + g_assert (kf.current_keys == NULL); + /* we can't add an automatic destroy handler for the value because we replace + * it when adding keys, and this means we'd have to copy the value each time + * we replace it */ + g_hash_table_foreach_remove (kf.groups, groups_hashtable_free, NULL); + g_hash_table_destroy (kf.groups); + g_free (kf.current_group); + g_string_free (kf.parse_buffer, TRUE); return (!kf.fatal_error); } /* return FALSE if we were unable to fix the file */ gboolean -desktop_file_fixup (GKeyFile *kf, +desktop_file_fixup (GKeyFile *keyfile, const char *filename) { char *value; unsigned int i; - if (g_key_file_has_group (kf, GROUP_KDE_DESKTOP_ENTRY)) { + if (g_key_file_has_group (keyfile, GROUP_KDE_DESKTOP_ENTRY)) { g_printerr ("%s: renaming deprecated \"%s\" group to \"%s\"\n", filename, GROUP_KDE_DESKTOP_ENTRY, GROUP_DESKTOP_ENTRY); - dfu_key_file_rename_group (kf, + dfu_key_file_rename_group (keyfile, GROUP_KDE_DESKTOP_ENTRY, GROUP_DESKTOP_ENTRY); } @@ -1911,7 +2221,7 @@ desktop_file_fixup (GKeyFile *kf, registered_desktop_keys[i].type != DESKTOP_REGEXP_LIST_TYPE) continue; - value = g_key_file_get_value (kf, GROUP_DESKTOP_ENTRY, + value = g_key_file_get_value (keyfile, GROUP_DESKTOP_ENTRY, registered_desktop_keys[i].name, NULL); if (value) { int len; @@ -1928,7 +2238,7 @@ desktop_file_fixup (GKeyFile *kf, filename, registered_desktop_keys[i].name); str = g_strconcat (value, ";", NULL); - g_key_file_set_value (kf, GROUP_DESKTOP_ENTRY, + g_key_file_set_value (keyfile, GROUP_DESKTOP_ENTRY, registered_desktop_keys[i].name, str); g_free (str); } diff --git a/src/validator.c b/src/validator.c index ec24c98..2ab5215 100644 --- a/src/validator.c +++ b/src/validator.c @@ -59,6 +59,8 @@ main (int argc, char *argv[]) return 1; } + g_option_context_free (context); + /* only accept one desktop file argument */ if (filename == NULL || filename[0] == NULL || filename[1] != NULL) { g_printerr ("See \"%s --help\" for correct usage.\n", g_get_prgname ()); |