fwupd/libfwupdplugin/fu-string.c

572 lines
13 KiB
C

/*
* Copyright (C) 2017 Richard Hughes <richard@hughsie.com>
*
* SPDX-License-Identifier: LGPL-2.1+
*/
#define G_LOG_DOMAIN "FuCommon"
#include "config.h"
#include "fu-string.h"
/**
* fu_strtoull:
* @str: a string, e.g. `0x1234`
* @value: (out) (nullable): parsed value
* @min: minimum acceptable value, typically 0
* @max: maximum acceptable value, typically G_MAXUINT64
* @error: (nullable): optional return location for an error
*
* Converts a string value to an integer. Values are assumed base 10, unless
* prefixed with "0x" where they are parsed as base 16.
*
* Returns: %TRUE if the value was parsed correctly, or %FALSE for error
*
* Since: 1.8.2
**/
gboolean
fu_strtoull(const gchar *str, guint64 *value, guint64 min, guint64 max, GError **error)
{
gchar *endptr = NULL;
guint64 value_tmp;
guint base = 10;
/* sanity check */
if (str == NULL) {
g_set_error_literal(error,
G_IO_ERROR,
G_IO_ERROR_INVALID_DATA,
"cannot parse NULL");
return FALSE;
}
/* detect hex */
if (g_str_has_prefix(str, "0x")) {
str += 2;
base = 16;
}
/* convert */
value_tmp = g_ascii_strtoull(str, &endptr, base);
if ((gsize)(endptr - str) != strlen(str) && *endptr != '\n') {
g_set_error(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "cannot parse %s", str);
return FALSE;
}
/* overflow check */
if (value_tmp == G_MAXUINT64) {
g_set_error(error,
G_IO_ERROR,
G_IO_ERROR_INVALID_DATA,
"cannot parse %s as caused overflow",
str);
return FALSE;
}
/* range check */
if (value_tmp < min) {
g_set_error(error,
G_IO_ERROR,
G_IO_ERROR_INVALID_DATA,
"value %" G_GUINT64_FORMAT " was below minimum %" G_GUINT64_FORMAT,
value_tmp,
min);
return FALSE;
}
if (value_tmp > max) {
g_set_error(error,
G_IO_ERROR,
G_IO_ERROR_INVALID_DATA,
"value %" G_GUINT64_FORMAT " was above maximum %" G_GUINT64_FORMAT,
value_tmp,
max);
return FALSE;
}
/* success */
if (value != NULL)
*value = value_tmp;
return TRUE;
}
/**
* fu_strtobool:
* @str: a string, e.g. `true`
* @value: (out) (nullable): parsed value
* @error: (nullable): optional return location for an error
*
* Converts a string value to a boolean. Only `true` and `false` are accepted values.
*
* Returns: %TRUE if the value was parsed correctly, or %FALSE for error
*
* Since: 1.8.2
**/
gboolean
fu_strtobool(const gchar *str, gboolean *value, GError **error)
{
/* sanity check */
if (str == NULL) {
g_set_error_literal(error,
G_IO_ERROR,
G_IO_ERROR_INVALID_DATA,
"cannot parse NULL");
return FALSE;
}
/* be super strict */
if (g_strcmp0(str, "true") == 0) {
if (value != NULL)
*value = TRUE;
return TRUE;
}
if (g_strcmp0(str, "false") == 0) {
if (value != NULL)
*value = FALSE;
return TRUE;
}
/* invalid */
g_set_error(error,
G_IO_ERROR,
G_IO_ERROR_INVALID_DATA,
"cannot parse %s as boolean, expected true|false",
str);
return FALSE;
}
/**
* fu_strstrip:
* @str: a string, e.g. ` test `
*
* Removes leading and trailing whitespace from a constant string.
*
* Returns: newly allocated string
*
* Since: 1.8.2
**/
gchar *
fu_strstrip(const gchar *str)
{
guint head = G_MAXUINT;
guint tail = 0;
g_return_val_if_fail(str != NULL, NULL);
/* find first non-space char */
for (guint i = 0; str[i] != '\0'; i++) {
if (str[i] != ' ') {
head = i;
break;
}
}
if (head == G_MAXUINT)
return g_strdup("");
/* find last non-space char */
for (guint i = head; str[i] != '\0'; i++) {
if (!g_ascii_isspace(str[i]))
tail = i;
}
return g_strndup(str + head, tail - head + 1);
}
/**
* fu_strdup:
* @str: a string, e.g. ` test `
* @bufsz: the maximum size of @str
* @offset: the offset to start copying from
*
* Copies a string from a buffer of a specified size up to (but not including) `NUL`.
*
* Returns: (transfer full): a #GString, possibly of zero size.
*
* Since: 1.8.11
**/
GString *
fu_strdup(const gchar *str, gsize bufsz, gsize offset)
{
GString *substr;
g_return_val_if_fail(str != NULL, NULL);
g_return_val_if_fail(offset < bufsz, NULL);
substr = g_string_new(NULL);
while (offset < bufsz) {
if (str[offset] == '\0')
break;
g_string_append_c(substr, str[offset++]);
}
return substr;
}
/**
* fu_string_replace:
* @string: the #GString to operate on
* @search: the text to search for
* @replace: the text to use for substitutions
*
* Performs multiple search and replace operations on the given string.
*
* Returns: the number of replacements done, or 0 if @search is not found.
*
* Since: 1.8.2
**/
guint
fu_string_replace(GString *string, const gchar *search, const gchar *replace)
{
#if GLIB_CHECK_VERSION(2, 68, 0)
g_return_val_if_fail(string != NULL, 0);
g_return_val_if_fail(search != NULL, 0);
g_return_val_if_fail(replace != NULL, 0);
return g_string_replace(string, search, replace, 0);
#else
gchar *tmp;
guint count = 0;
gsize search_idx = 0;
gsize replace_len;
gsize search_len;
g_return_val_if_fail(string != NULL, 0);
g_return_val_if_fail(search != NULL, 0);
g_return_val_if_fail(replace != NULL, 0);
/* nothing to do */
if (string->len == 0)
return 0;
search_len = strlen(search);
replace_len = strlen(replace);
do {
tmp = g_strstr_len(string->str + search_idx, -1, search);
if (tmp == NULL)
break;
/* advance the counter in case @replace contains @search */
search_idx = (gsize)(tmp - string->str);
/* reallocate the string if required */
if (search_len > replace_len) {
g_string_erase(string,
(gssize)search_idx,
(gssize)(search_len - replace_len));
memcpy(tmp, replace, replace_len);
} else if (search_len < replace_len) {
g_string_insert_len(string,
(gssize)search_idx,
replace,
(gssize)(replace_len - search_len));
/* we have to treat this specially as it could have
* been reallocated when the insertion happened */
memcpy(string->str + search_idx, replace, replace_len);
} else {
/* just memcmp in the new string */
memcpy(tmp, replace, replace_len);
}
search_idx += replace_len;
count++;
} while (TRUE);
return count;
#endif
}
/**
* fu_strwidth:
* @text: the string to operate on
*
* Returns the width of the string in displayed characters on the console.
*
* Returns: width of text
*
* Since: 1.8.2
**/
gsize
fu_strwidth(const gchar *text)
{
const gchar *p = text;
gsize width = 0;
g_return_val_if_fail(text != NULL, 0);
while (*p) {
gunichar c = g_utf8_get_char(p);
if (g_unichar_iswide(c))
width += 2;
else if (!g_unichar_iszerowidth(c))
width += 1;
p = g_utf8_next_char(p);
}
return width;
}
/**
* fu_string_append:
* @str: a #GString
* @idt: the indent
* @key: a string to append
* @value: a string to append
*
* Appends a key and string value to a string
*
* Since: 1.8.2
*/
void
fu_string_append(GString *str, guint idt, const gchar *key, const gchar *value)
{
const guint align = 24;
gsize keysz;
g_return_if_fail(idt * 2 < align);
/* ignore */
if (key == NULL)
return;
for (gsize i = 0; i < idt; i++)
g_string_append(str, " ");
if (key[0] != '\0') {
g_string_append_printf(str, "%s:", key);
keysz = (idt * 2) + fu_strwidth(key) + 1;
} else {
keysz = idt * 2;
}
if (value != NULL) {
g_auto(GStrv) split = NULL;
split = g_strsplit(value, "\n", -1);
for (guint i = 0; split[i] != NULL; i++) {
if (i == 0) {
for (gsize j = keysz; j < align; j++)
g_string_append(str, " ");
} else {
g_string_append(str, "\n");
for (gsize j = 0; j < idt; j++)
g_string_append(str, " ");
}
g_string_append(str, split[i]);
}
}
g_string_append(str, "\n");
}
/**
* fu_string_append_ku:
* @str: a #GString
* @idt: the indent
* @key: a string to append
* @value: guint64
*
* Appends a key and unsigned integer to a string
*
* Since: 1.8.2
*/
void
fu_string_append_ku(GString *str, guint idt, const gchar *key, guint64 value)
{
g_autofree gchar *tmp = g_strdup_printf("%" G_GUINT64_FORMAT, value);
fu_string_append(str, idt, key, tmp);
}
/**
* fu_string_append_kx:
* @str: a #GString
* @idt: the indent
* @key: a string to append
* @value: guint64
*
* Appends a key and hex integer to a string
*
* Since: 1.8.2
*/
void
fu_string_append_kx(GString *str, guint idt, const gchar *key, guint64 value)
{
g_autofree gchar *tmp = g_strdup_printf("0x%x", (guint)value);
fu_string_append(str, idt, key, tmp);
}
/**
* fu_string_append_kb:
* @str: a #GString
* @idt: the indent
* @key: a string to append
* @value: Boolean
*
* Appends a key and boolean value to a string
*
* Since: 1.8.2
*/
void
fu_string_append_kb(GString *str, guint idt, const gchar *key, gboolean value)
{
fu_string_append(str, idt, key, value ? "true" : "false");
}
/**
* fu_strsplit:
* @str: (not nullable): a string to split
* @sz: size of @str, which must be more than 0
* @delimiter: a string which specifies the places at which to split the string
* @max_tokens: the maximum number of pieces to split @str into
*
* Splits a string into a maximum of @max_tokens pieces, using the given
* delimiter. If @max_tokens is reached, the remainder of string is appended
* to the last token.
*
* Returns: (transfer full): a newly-allocated NULL-terminated array of strings
*
* Since: 1.8.2
**/
gchar **
fu_strsplit(const gchar *str, gsize sz, const gchar *delimiter, gint max_tokens)
{
g_return_val_if_fail(str != NULL, NULL);
g_return_val_if_fail(sz > 0, NULL);
if (str[sz - 1] != '\0') {
g_autofree gchar *str2 = g_strndup(str, sz);
return g_strsplit(str2, delimiter, max_tokens);
}
return g_strsplit(str, delimiter, max_tokens);
}
/**
* fu_strsplit_full:
* @str: a string to split
* @sz: size of @str, or -1 for unknown
* @delimiter: a string which specifies the places at which to split the string
* @callback: (scope call) (closure user_data): a #FuStrsplitFunc.
* @user_data: user data
* @error: (nullable): optional return location for an error
*
* Splits the string, calling the given function for each
* of the tokens found. If any @callback returns %FALSE scanning is aborted.
*
* Use this function in preference to fu_strsplit() when the input file is untrusted,
* and you don't want to allocate a GStrv with billions of one byte items.
*
* Returns: %TRUE if no @callback returned FALSE
*
* Since: 1.8.2
*/
gboolean
fu_strsplit_full(const gchar *str,
gssize sz,
const gchar *delimiter,
FuStrsplitFunc callback,
gpointer user_data,
GError **error)
{
gsize delimiter_sz;
gsize str_sz;
guint found_idx = 0;
guint token_idx = 0;
g_return_val_if_fail(str != NULL, FALSE);
g_return_val_if_fail(delimiter != NULL && delimiter[0] != '\0', FALSE);
g_return_val_if_fail(callback != NULL, FALSE);
g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
/* make known */
str_sz = sz != -1 ? (gsize)sz : strlen(str);
delimiter_sz = strlen(delimiter);
/* cannot split */
if (delimiter_sz > str_sz) {
g_autoptr(GString) token = g_string_new(str);
return callback(token, token_idx, user_data, error);
}
/* start splittin' */
for (gsize i = 0; i < (str_sz - delimiter_sz) + 1;) {
if (strncmp(str + i, delimiter, delimiter_sz) == 0) {
g_autoptr(GString) token = g_string_new(NULL);
g_string_append_len(token, str + found_idx, i - found_idx);
if (!callback(token, token_idx++, user_data, error))
return FALSE;
i += delimiter_sz;
found_idx = i;
} else {
i++;
}
}
/* any bits left over? */
if (found_idx != str_sz) {
g_autoptr(GString) token = g_string_new(NULL);
g_string_append_len(token, str + found_idx, str_sz - found_idx);
if (!callback(token, token_idx, user_data, error))
return FALSE;
}
/* success */
return TRUE;
}
/**
* fu_strsafe:
* @str: (nullable): a string to make safe for printing
* @maxsz: maximum size of returned string
*
* Converts a string into something that can be safely printed.
*
* Returns: (transfer full): safe string, or %NULL if there was nothing valid
*
* Since: 1.8.2
**/
gchar *
fu_strsafe(const gchar *str, gsize maxsz)
{
gboolean valid = FALSE;
g_autoptr(GString) tmp = NULL;
/* sanity check */
if (str == NULL || maxsz == 0)
return NULL;
/* replace non-printable chars with '.' */
tmp = g_string_sized_new(maxsz);
for (gsize i = 0; i < maxsz && str[i] != '\0'; i++) {
if (!g_ascii_isprint(str[i])) {
g_string_append_c(tmp, '.');
continue;
}
g_string_append_c(tmp, str[i]);
if (!g_ascii_isspace(str[i]))
valid = TRUE;
}
/* if just junk, don't return 'all dots' */
if (tmp->len == 0 || !valid)
return NULL;
return g_string_free(g_steal_pointer(&tmp), FALSE);
}
/**
* fu_strjoin:
* @separator: (nullable): string to insert between each of the strings
* @array: (element-type utf8): a #GPtrArray
*
* Joins an array of strings together to form one long string, with the optional
* separator inserted between each of them.
*
* If @array has no items, the return value will be an empty string.
* If @array contains a single item, separator will not appear in the resulting
* string.
*
* Returns: a string
*
* Since: 1.8.2
**/
gchar *
fu_strjoin(const gchar *separator, GPtrArray *array)
{
g_autofree const gchar **strv = NULL;
g_return_val_if_fail(array != NULL, NULL);
strv = g_new0(const gchar *, array->len + 1);
for (guint i = 0; i < array->len; i++)
strv[i] = g_ptr_array_index(array, i);
return g_strjoinv(separator, (gchar **)strv);
}