/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- * * Copyright (C) 2015 Richard Hughes * * Licensed under the GNU Lesser General Public License Version 2.1 * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ /** * SECTION:as-inf * @short_description: Helper functions for parsing .inf files * @include: appstream-glib.h * @stability: Stable * * These functions are used internally to libappstream-glib, and some may be * useful to user-applications. */ #include "config.h" #include #include #include #include "as-inf.h" /** * as_inf_error_quark: * * Return value: An error quark. * * Since: 0.3.7 **/ G_DEFINE_QUARK (as-inf-error-quark, as_inf_error) typedef struct { GKeyFile *keyfile; GHashTable *dict; gboolean last_line_continuation; gboolean last_line_continuation_ignore; gboolean require_2nd_pass; gchar *group; gchar *last_key; gchar *comment; gchar *comment_group; guint nokey_idx; AsInfLoadFlags flags; } AsInfHelper; static gchar * as_inf_make_case_insensitive (AsInfHelper *helper, const gchar *text) { if (helper->flags & AS_INF_LOAD_FLAG_CASE_INSENSITIVE) return g_ascii_strdown (text, -1); return g_strdup (text); } static gboolean as_inf_string_isdigits (const gchar *str) { guint i; for (i = 0; str[i] != '\0'; i++) { if (!g_ascii_isdigit (str[i])) return FALSE; } return TRUE; } static gchar * as_inf_replace_variable (AsInfHelper *helper, const gchar *line, GError **error) { GString *new; const gchar *tmp; guint i; g_auto(GStrv) split = NULL; /* split up into sections of the delimiter */ new = g_string_sized_new (strlen (line)); split = g_strsplit (line, "%", -1); for (i = 0; split[i] != NULL; i++) { g_autofree gchar *lower = NULL; /* the text between the substitutions */ if (i % 2 == 0) { g_strdelimit (split[i], "\\", '/'); g_string_append (new, split[i]); continue; } /* a Dirid */ if (as_inf_string_isdigits (split[i])) { g_string_append (new, "/tmp"); continue; } /* replace or ignore things not (yet) in the dictionary */ lower = as_inf_make_case_insensitive (helper, split[i]); tmp = g_hash_table_lookup (helper->dict, lower); if (tmp == NULL) { if (helper->flags & AS_INF_LOAD_FLAG_STRICT) { g_set_error (error, AS_INF_ERROR, AS_INF_ERROR_FAILED, "No replacement for '%s' in [Strings]", lower); g_string_free (new, TRUE); return NULL; } g_string_append_printf (new, "%%%s%%", split[i]); continue; } g_string_append (new, tmp); } return g_string_free (new, FALSE); } static GHashTable * as_inf_get_dict (AsInfHelper *helper, GError **error) { GHashTable *dict = NULL; gchar *val; guint i; g_autofree gchar *lower = NULL; g_autoptr(GHashTable) dict_tmp = NULL; g_auto(GStrv) keys = NULL; struct { const gchar *search; const gchar *replace; } types[] = { { "REG_SZ", "0x00000000" }, { "REG_BINARY", "0x00000001" }, { "REG_MULTI_SZ", "0x00010000" }, { "REG_EXPAND_SZ", "0x00020000" }, { "REG_DWORD", "0x00010001" }, { "REG_NONE", "0x00020001" }, { NULL, NULL } }; dict_tmp = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); lower = as_inf_make_case_insensitive (helper, "Strings"); keys = g_key_file_get_keys (helper->keyfile, lower, NULL, NULL); for (i = 0; keys != NULL && keys[i] != NULL; i++) { val = g_key_file_get_string (helper->keyfile, lower, keys[i], error); if (val == NULL) goto out; g_hash_table_insert (dict_tmp, g_strdup (keys[i]), val); } /* verify that commonly missed types are present */ for (i = 0; types[i].search != NULL; i++) { if (g_hash_table_lookup (dict_tmp, types[i].search) == NULL) { g_hash_table_insert (dict_tmp, g_strdup (types[i].search), g_strdup (types[i].replace)); } } dict = g_hash_table_ref (dict_tmp); out: return dict; } static gboolean as_inf_replace_variables (AsInfHelper *helper, GError **error) { guint i; guint j; g_auto(GStrv) groups = NULL; groups = g_key_file_get_groups (helper->keyfile, NULL); for (i = 0; groups[i] != NULL; i++) { g_auto(GStrv) keys = NULL; /* ignore the source */ if (g_strcmp0 (groups[i], "Strings") == 0) continue; /* fix up keys */ keys = g_key_file_get_keys (helper->keyfile, groups[i], NULL, NULL); if (keys == NULL) continue; for (j = 0; keys[j] != NULL; j++) { g_autofree gchar *data_old = NULL; g_autofree gchar *data_new = NULL; /* get the old data for this [group] key */ data_old = g_key_file_get_string (helper->keyfile, groups[i], keys[j], NULL); if (data_old == NULL || data_old[0] == '\0') continue; /* optimise a little */ if (g_strstr_len (data_old, -1, "%") == NULL) continue; /* do string replacement */ data_new = as_inf_replace_variable (helper, data_old, error); if (data_new == NULL) return FALSE; if (g_strcmp0 (data_old, data_new) == 0) continue; g_key_file_set_string (helper->keyfile, groups[i], keys[j], data_new); } } return TRUE; } static void as_inf_set_last_key (AsInfHelper *helper, const gchar *key) { /* same value */ if (key == helper->last_key) return; g_free (helper->last_key); helper->last_key = g_strdup (key); } static void as_inf_set_comment (AsInfHelper *helper, const gchar *comment) { /* same value */ if (comment == helper->comment) return; g_free (helper->comment); helper->comment = g_strdup (comment); if (helper->comment != NULL) g_strchug (helper->comment); } static gboolean as_inf_strcheckchars (const gchar *str, const gchar *chars) { guint i; guint j; for (i = 0; str[i] != '\0'; i++) { for (j = 0; chars[j] != '\0'; j++) { if (str[i] == chars[j]) return TRUE; } } return FALSE; } static gboolean as_inf_strip_value (AsInfHelper *helper, gchar *value, GError **error) { gchar *comment; gchar *last_quote; /* trivial case; no quotes */ if (value[0] != '"') { comment = g_strrstr (value + 1, ";"); if (comment != NULL) { *comment = '\0'; if (helper->comment == NULL) as_inf_set_comment (helper, comment + 1); g_strchomp (value + 1); } return TRUE; } /* is there only one quote? */ last_quote = g_strrstr (value, "\""); if (last_quote == value) { if (helper->flags & AS_INF_LOAD_FLAG_STRICT) { g_set_error (error, AS_INF_ERROR, AS_INF_ERROR_FAILED, "mismatched quotes %s", value); return FALSE; } last_quote = NULL; } /* shift the string down */ memmove (value, value + 1, strlen (value)); /* there's a comment past the last quote */ comment = g_strrstr (value, ";"); if (comment != NULL && comment > last_quote) { *comment = '\0'; if (helper->comment == NULL) as_inf_set_comment (helper, comment + 1); g_strchomp (value); } /* remove the last quote */ if (last_quote != NULL) *(last_quote-1) = '\0'; return TRUE; } static gboolean as_inf_set_group (AsInfHelper *helper, const gchar *group, GError **error) { /* same value */ if (group == helper->group) return TRUE; /* maximum permitted length is 255 chars */ if (strlen (group) > 255) { if (helper->flags & AS_INF_LOAD_FLAG_STRICT) { g_set_error (error, AS_INF_ERROR, AS_INF_ERROR_FAILED, "section name is too long: %s", group); return FALSE; } } /* save new value */ g_free (helper->group); helper->group = as_inf_make_case_insensitive (helper, group); /* section names can contain double quotes */ if (!as_inf_strip_value (helper, helper->group, error)) return FALSE; /* does this section name have any banned chars */ if (helper->flags & AS_INF_LOAD_FLAG_STRICT) { if (g_str_has_prefix (helper->group, " ")) { g_set_error (error, AS_INF_ERROR, AS_INF_ERROR_FAILED, "section name '%s' has leading spaces", helper->group); return FALSE; } if (g_str_has_suffix (helper->group, " ")) { g_set_error (error, AS_INF_ERROR, AS_INF_ERROR_FAILED, "section name '%s' has trailing spaces", helper->group); return FALSE; } if (as_inf_strcheckchars (helper->group, "\t\n\r;\"")) { g_set_error (error, AS_INF_ERROR, AS_INF_ERROR_FAILED, "section name '%s' has invalid chars", helper->group); return FALSE; } } else { g_strstrip (helper->group); g_strdelimit (helper->group, "\t\n\r;\"", '_'); } /* start with new values */ helper->last_line_continuation_ignore = FALSE; helper->nokey_idx = 0; /* no longer valid */ as_inf_set_last_key (helper, NULL); /* FIXME: refusing to add a comment to a group that does not exist * is a GLib bug! */ if (helper->comment != NULL) { g_key_file_set_comment (helper->keyfile, helper->group, NULL, helper->comment, NULL); as_inf_set_comment (helper, NULL); } return TRUE; } static void as_inf_set_key (AsInfHelper *helper, const gchar *key, const gchar *value) { helper->last_line_continuation_ignore = FALSE; g_key_file_set_string (helper->keyfile, helper->group, key, value); if (helper->comment != NULL) { g_key_file_set_comment (helper->keyfile, helper->group, key, helper->comment, NULL); as_inf_set_comment (helper, NULL); } as_inf_set_last_key (helper, key); } static void as_inf_convert_key (gchar *key) { guint i; for (i = 0; key[i] != '\0'; i++) { if (g_ascii_isalnum (key[i])) continue; if (key[i] == '.' || key[i] == '%') continue; key[i] = '_'; } } static gboolean as_inf_repair_utf8 (AsInfHelper *helper, gchar *line, GError **error) { guint i; guint8 val; /* Microsoft keeps using 0x99 as UTF-8 (R), and it's not valid */ for (i = 0; line[i] != '\0'; i++) { val = (guint8) line[i]; if (val == 0x99) { if (helper->flags & AS_INF_LOAD_FLAG_STRICT) { g_set_error (error, AS_INF_ERROR, AS_INF_ERROR_FAILED, "Invalid UTF-8: 0x%02x", val); return FALSE; } line[i] = '?'; continue; } } return TRUE; } static gboolean as_inf_parse_line (AsInfHelper *helper, gchar *line, GError **error) { gchar *kvsplit; gchar *comment; gchar *tmp; guint len; gboolean ret = TRUE; gboolean continuation = FALSE; g_autofree gchar *key = NULL; /* line too long */ if ((helper->flags & AS_INF_LOAD_FLAG_STRICT) > 0 && strlen (line) > 4096) { ret = FALSE; g_set_error (error, AS_INF_ERROR, AS_INF_ERROR_FAILED, "line too long: %s", line); goto out; } /* fix up invalid UTF-8 */ if (!as_inf_repair_utf8 (helper, line, error)) { ret = FALSE; goto out; } /* remove leading and trailing whitespace */ g_strstrip (line); if (line[0] == '\0') goto out; /* remove comments */ if (line[0] == ';') { as_inf_set_comment (helper, line + 1); goto out; } /* ignore pragmas */ if (g_str_has_prefix (line, "#pragma")) goto out; if (g_str_has_prefix (line, "#define")) goto out; /* [group] */ if (line[0] == '[') { /* remove comments */ comment = g_strstr_len (line, -1, ";"); if (comment != NULL) { *comment = '\0'; as_inf_set_comment (helper, comment + 1); g_strchomp (line); } /* find group end */ tmp = g_strrstr (line, "]"); if (tmp == NULL) { ret = FALSE; g_set_error (error, AS_INF_ERROR, AS_INF_ERROR_FAILED, "[ but no ] : '%s'", line); goto out; } *tmp = '\0'; ret = as_inf_set_group (helper, line + 1, error); goto out; } /* last char continuation */ len = (guint) strlen (line); if (line[len-1] == '\\') { line[len-1] = '\0'; continuation = TRUE; } /* is a multiline key value */ if (line[len-1] == '|') { line[len-1] = '\0'; continuation = TRUE; } /* if the key-value split is before the first quote */ kvsplit = g_strstr_len (line, -1, "="); tmp = g_strstr_len (line, -1, "\""); if ((tmp == NULL && kvsplit != NULL) || (kvsplit != NULL && kvsplit < tmp)) { g_autofree gchar *key_new = NULL; /* key=value before [group] */ if (helper->group == NULL) { ret = FALSE; g_set_error (error, AS_INF_ERROR, AS_INF_ERROR_FAILED, "key not in group: '%s'", line); goto out; } *kvsplit = '\0'; key = as_inf_make_case_insensitive (helper, line); g_strchomp (key); /* remove invalid chars as GKeyFile is more strict than .ini */ as_inf_convert_key (key); /* convert key names with variables */ if (key[0] == '%') { g_autofree gchar *key_tmp = NULL; if (helper->dict == NULL) { helper->require_2nd_pass = TRUE; helper->last_line_continuation_ignore = TRUE; goto out; } key_tmp = as_inf_replace_variable (helper, key, error); if (key_tmp == NULL) return FALSE; if (key_tmp[0] == '%') { key_new = g_strdup (key + 1); g_strdelimit (key_new, "%", '\0'); g_debug ("No replacement for '%s' in [Strings] " "using '%s'", key, key_new); goto out; } /* this has to be lowercase */ key_new = as_inf_make_case_insensitive (helper, key_tmp); as_inf_convert_key (key_new); } else { key_new = g_strdup (key); } /* remove leading and trailing quote */ g_strchug (kvsplit + 1); if (!as_inf_strip_value (helper, kvsplit + 1, error)) { ret = FALSE; goto out; } /* add to keyfile */ as_inf_set_key (helper, key_new, kvsplit + 1); goto out; } /* last_line_continuation from the last line */ if (helper->last_line_continuation) { g_autofree gchar *old = NULL; g_autofree gchar *new = NULL; /* this is the 1st pass, and we have no key */ if (helper->last_line_continuation_ignore) goto out; /* eek, no key at all */ if (helper->last_key == NULL) { ret = FALSE; g_set_error (error, AS_INF_ERROR, AS_INF_ERROR_FAILED, "continued last but no key for %s", line); goto out; } /* get the old value of this */ old = g_key_file_get_string (helper->keyfile, helper->group, helper->last_key, NULL); if (old == NULL) { ret = FALSE; g_set_error (error, AS_INF_ERROR, AS_INF_ERROR_FAILED, "can't continue [%s] '%s': %s", helper->group, helper->last_key, line); goto out; } /* remove leading and trailing quote */ if (!as_inf_strip_value (helper, line, error)) { ret = FALSE; goto out; } /* re-set key with new value */ new = g_strdup_printf ("%s%s", old, line); as_inf_set_key (helper, helper->last_key, new); goto out; } /* there was left-over data but with no group defined */ if (helper->group == NULL) { ret = FALSE; g_set_error (error, AS_INF_ERROR, AS_INF_ERROR_FAILED, "fake key %s without [group]", line); goto out; } /* remove leading and trailing quote */ if (!as_inf_strip_value (helper, line, error)) { ret = FALSE; goto out; } /* convert registry entries. e.g. * "HKR,,FirmwareFilename,,firmware.bin" * -> "HKR_FirmwareFilename"="firmware.bin" * "HKR,,FirmwareVersion,%REG_DWORD%,0x0000000" * -> "HKR_FirmwareVersion_0x00010001"="0x0000000" */ if (g_strcmp0 (helper->group, "Firmware_AddReg") == 0 && g_str_has_prefix (line, "HK")) { guint i; g_auto(GStrv) reg_split = NULL; g_autoptr(GString) str = NULL; str = g_string_new (""); reg_split = g_strsplit (line, ",", -1); for (i = 0; reg_split[i+1] != NULL; i++) { g_strstrip (reg_split[i]); if (reg_split[i][0] == '\0') continue; g_string_append_printf (str, "%s_", reg_split[i]); } if (str->len > 0) { g_autofree gchar *key_tmp = NULL; /* remove trailing '_' */ g_string_truncate (str, str->len - 1); /* remove leading and trailing quote */ g_strchug (reg_split[i]); if (!as_inf_strip_value (helper, reg_split[i], error)) { ret = FALSE; goto out; } if (helper->dict == NULL) { helper->require_2nd_pass = TRUE; helper->last_line_continuation_ignore = TRUE; goto out; } key_tmp = as_inf_replace_variable (helper, str->str, error); if (key_tmp == NULL) return FALSE; as_inf_set_key (helper, key_tmp, reg_split[i]); goto out; } } /* add fake key */ key = g_strdup_printf ("value%03u", helper->nokey_idx++); as_inf_set_key (helper, key, line); out: helper->last_line_continuation = continuation; return ret; } static AsInfHelper * as_inf_helper_new (void) { AsInfHelper *helper; helper = g_new0 (AsInfHelper, 1); return helper; } static void as_inf_helper_free (AsInfHelper *helper) { if (helper->dict != NULL) g_hash_table_unref (helper->dict); g_key_file_unref (helper->keyfile); g_free (helper->comment); g_free (helper->group); g_free (helper->last_key); g_free (helper); } /** * as_inf_load_data: * @keyfile: a #GKeyFile * @data: the .inf file date to parse * @flags: #AsInfLoadFlags, e.g. %AS_INF_LOAD_FLAG_NONE * @error: A #GError or %NULL * * Repairs .inf file data and opens it as a keyfile. * * Important: The group and keynames are all forced to lower case as INF files * are specified as case insensitve and GKeyFile *is* case sensitive. * Any backslashes or spaces in the key name are also converted to '_'. * * Returns: %TRUE for success * * Since: 0.3.5 */ gboolean as_inf_load_data (GKeyFile *keyfile, const gchar *data, AsInfLoadFlags flags, GError **error) { AsInfHelper *helper; gboolean ret = TRUE; guint i; g_auto(GStrv) lines = NULL; /* initialize helper */ helper = as_inf_helper_new (); helper->flags = flags; helper->keyfile = g_key_file_ref (keyfile); /* verify each line, and make sane */ lines = g_strsplit (data, "\n", -1); for (i = 0; lines[i] != NULL; i++) { if (!as_inf_parse_line (helper, lines[i], error)) { g_prefix_error (error, "Failed to parse line %u: ", i + 1); ret = FALSE; goto out; } } /* process every key in each group for strings we can substitute */ helper->dict = as_inf_get_dict (helper, error); if (helper->dict == NULL) { ret = FALSE; goto out; } /* lets do this all over again */ if (helper->require_2nd_pass) { g_auto(GStrv) lines2 = NULL; lines2 = g_strsplit (data, "\n", -1); for (i = 0; lines2[i] != NULL; i++) { if (!as_inf_parse_line (helper, lines2[i], error)) { g_prefix_error (error, "Failed to parse line %u: ", i + 1); ret = FALSE; goto out; } } } /* replace key values */ if (!as_inf_replace_variables (helper, error)) { ret = FALSE; goto out; } out: as_inf_helper_free (helper); return ret; } typedef struct { const gchar *id; const gchar *name; guint id_length; } AsInfBOM; /** * as_inf_load_file: * @keyfile: a #GKeyFile * @filename: the .inf file to open * @flags: #AsInfLoadFlags, e.g. %AS_INF_LOAD_FLAG_NONE * @error: A #GError or %NULL * * Repairs an .inf file and opens it as a keyfile. * * Returns: %TRUE for success * * Since: 0.3.5 */ gboolean as_inf_load_file (GKeyFile *keyfile, const gchar *filename, AsInfLoadFlags flags, GError **error) { const gchar *data_no_bom; gsize len; guint i; g_autofree gchar *data = NULL; AsInfBOM boms[] = { { "\x00\x00\xfe\xff", "UTF-32BE", 4 }, { "\xff\xfe\x00\x00", "UTF-32LE", 4 }, { "\xfe\xff", "UTF-16BE", 2 }, { "\xff\xfe", "UTF-16LE", 2 }, { "\xef\xbb\xbf", "UTF-8", 3 }, { NULL, NULL, 0 } }; if (!g_file_get_contents (filename, &data, &len, error)) return FALSE; /* detect BOM */ data_no_bom = data; for (i = 0; boms[i].id != NULL; i++) { /* BOM matches */ if (len < boms[i].id_length) continue; if (memcmp (data, boms[i].id, boms[i].id_length) != 0) continue; /* ignore UTF-8 BOM */ if (g_strcmp0 (boms[i].name, "UTF-8") == 0) { data_no_bom += boms[i].id_length; break; } g_set_error (error, AS_INF_ERROR, AS_INF_ERROR_INVALID_TYPE, "File is encoded with %s and not supported", boms[i].name); return FALSE; } /* load data stripped of BOM */ return as_inf_load_data (keyfile, data_no_bom, flags, error); } /** * as_inf_get_driver_version: * @keyfile: a #GKeyFile * @timestamp: the returned driverver timestamp, or %NULL * @error: A #GError or %NULL * * Parses the DriverVer string into a recognisable version and timestamp; * * Returns: the version string, or %NULL for error. * * Since: 0.3.5 */ gchar * as_inf_get_driver_version (GKeyFile *keyfile, guint64 *timestamp, GError **error) { g_autoptr(GDateTime) dt = NULL; g_auto(GStrv) split = NULL; g_auto(GStrv) dv_split = NULL; g_autofree gchar *driver_ver = NULL; /* get the release date and the version in case there's no metainfo */ driver_ver = g_key_file_get_string (keyfile, "Version", "DriverVer", NULL); if (driver_ver == NULL) { g_set_error_literal (error, AS_INF_ERROR, AS_INF_ERROR_NOT_FOUND, "DriverVer is missing"); return NULL; } /* split into driver date and version */ dv_split = g_strsplit (driver_ver, ",", -1); if (g_strv_length (dv_split) != 2) { g_set_error (error, AS_INF_ERROR, AS_INF_ERROR_INVALID_TYPE, "DriverVer is invalid: %s", driver_ver); return NULL; } /* split up into MM/DD/YYYY, because America */ if (timestamp != NULL) { split = g_strsplit (dv_split[0], "/", -1); if (g_strv_length (split) != 3) { g_set_error (error, AS_INF_ERROR, AS_INF_ERROR_INVALID_TYPE, "DriverVer date invalid: %s", dv_split[0]); return NULL; } dt = g_date_time_new_utc (atoi (split[2]), atoi (split[0]), atoi (split[1]), 0, 0, 0); if (dt == NULL) { g_set_error (error, AS_INF_ERROR, AS_INF_ERROR_INVALID_TYPE, "DriverVer date invalid: %s", dv_split[0]); return NULL; } *timestamp = (guint64) g_date_time_to_unix (dt); } return g_strdup (dv_split[1]); }