fwupd/libfwupdplugin/fu-version-common.c
2022-12-11 20:25:52 +00:00

654 lines
18 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 <string.h>
#include "fwupd-enums.h"
#include "fwupd-error.h"
#include "fu-version-common.h"
#define FU_COMMON_VERSION_DECODE_BCD(val) ((((val) >> 4) & 0x0f) * 10 + ((val)&0x0f))
gchar *
fu_common_version_ensure_semver(const gchar *version);
/**
* fu_version_from_uint64:
* @val: a raw version number
* @kind: version kind used for formatting, e.g. %FWUPD_VERSION_FORMAT_QUAD
*
* Returns a dotted decimal version string from a 64 bit number.
*
* Returns: a version number, e.g. `1.2.3.4`, or %NULL if not supported
*
* Since: 1.8.2
**/
gchar *
fu_version_from_uint64(guint64 val, FwupdVersionFormat kind)
{
if (kind == FWUPD_VERSION_FORMAT_QUAD) {
/* AABB.CCDD.EEFF.GGHH */
return g_strdup_printf("%" G_GUINT64_FORMAT "."
"%" G_GUINT64_FORMAT "."
"%" G_GUINT64_FORMAT "."
"%" G_GUINT64_FORMAT "",
(val >> 48) & 0xffff,
(val >> 32) & 0xffff,
(val >> 16) & 0xffff,
val & 0xffff);
}
if (kind == FWUPD_VERSION_FORMAT_PAIR) {
/* AABBCCDD.EEFFGGHH */
return g_strdup_printf("%" G_GUINT64_FORMAT ".%" G_GUINT64_FORMAT "",
(val >> 32) & 0xffffffff,
val & 0xffffffff);
}
if (kind == FWUPD_VERSION_FORMAT_NUMBER || kind == FWUPD_VERSION_FORMAT_PLAIN) {
/* AABBCCDD */
return g_strdup_printf("%" G_GUINT64_FORMAT, val);
}
if (kind == FWUPD_VERSION_FORMAT_HEX) {
/* 0xAABBCCDDEEFFGGHH */
return g_strdup_printf("0x%08x%08x",
(guint32)(val >> 32),
(guint32)(val & 0xffffffff));
}
g_critical("failed to convert version format %s: %" G_GUINT64_FORMAT "",
fwupd_version_format_to_string(kind),
val);
return NULL;
}
/**
* fu_version_from_uint32:
* @val: a uint32le version number
* @kind: version kind used for formatting, e.g. %FWUPD_VERSION_FORMAT_TRIPLET
*
* Returns a dotted decimal version string from a 32 bit number.
*
* Returns: a version number, e.g. `1.0.3`, or %NULL if not supported
*
* Since: 1.8.2
**/
gchar *
fu_version_from_uint32(guint32 val, FwupdVersionFormat kind)
{
if (kind == FWUPD_VERSION_FORMAT_QUAD) {
/* AA.BB.CC.DD */
return g_strdup_printf("%u.%u.%u.%u",
(val >> 24) & 0xff,
(val >> 16) & 0xff,
(val >> 8) & 0xff,
val & 0xff);
}
if (kind == FWUPD_VERSION_FORMAT_TRIPLET) {
/* AA.BB.CCDD */
return g_strdup_printf("%u.%u.%u",
(val >> 24) & 0xff,
(val >> 16) & 0xff,
val & 0xffff);
}
if (kind == FWUPD_VERSION_FORMAT_PAIR) {
/* AABB.CCDD */
return g_strdup_printf("%u.%u", (val >> 16) & 0xffff, val & 0xffff);
}
if (kind == FWUPD_VERSION_FORMAT_NUMBER || kind == FWUPD_VERSION_FORMAT_PLAIN) {
/* AABBCCDD */
return g_strdup_printf("%" G_GUINT32_FORMAT, val);
}
if (kind == FWUPD_VERSION_FORMAT_BCD) {
/* AA.BB.CC.DD, but BCD */
return g_strdup_printf("%u.%u.%u.%u",
FU_COMMON_VERSION_DECODE_BCD(val >> 24),
FU_COMMON_VERSION_DECODE_BCD(val >> 16),
FU_COMMON_VERSION_DECODE_BCD(val >> 8),
FU_COMMON_VERSION_DECODE_BCD(val));
}
if (kind == FWUPD_VERSION_FORMAT_INTEL_ME) {
/* aaa+11.bbbbb.cccccccc.dddddddddddddddd */
return g_strdup_printf("%u.%u.%u.%u",
((val >> 29) & 0x07) + 0x0b,
(val >> 24) & 0x1f,
(val >> 16) & 0xff,
val & 0xffff);
}
if (kind == FWUPD_VERSION_FORMAT_INTEL_ME2) {
/* A.B.CC.DDDD */
return g_strdup_printf("%u.%u.%u.%u",
(val >> 28) & 0x0f,
(val >> 24) & 0x0f,
(val >> 16) & 0xff,
val & 0xffff);
}
if (kind == FWUPD_VERSION_FORMAT_SURFACE_LEGACY) {
/* 10b.12b.10b */
return g_strdup_printf("%u.%u.%u",
(val >> 22) & 0x3ff,
(val >> 10) & 0xfff,
val & 0x3ff);
}
if (kind == FWUPD_VERSION_FORMAT_SURFACE) {
/* 8b.16b.8b */
return g_strdup_printf("%u.%u.%u",
(val >> 24) & 0xff,
(val >> 8) & 0xffff,
val & 0xff);
}
if (kind == FWUPD_VERSION_FORMAT_DELL_BIOS) {
/* BB.CC.DD */
return g_strdup_printf("%u.%u.%u",
(val >> 16) & 0xff,
(val >> 8) & 0xff,
val & 0xff);
}
if (kind == FWUPD_VERSION_FORMAT_HEX) {
/* 0xAABBCCDD */
return g_strdup_printf("0x%08x", val);
}
g_critical("failed to convert version format %s: %u",
fwupd_version_format_to_string(kind),
val);
return NULL;
}
/**
* fu_version_from_uint24:
* @val: a uint24le version number
* @kind: version kind used for formatting, e.g. %FWUPD_VERSION_FORMAT_TRIPLET
*
* Returns a dotted decimal version string from a 24 bit number.
*
* Returns: a version number, e.g. `1.0.3`, or %NULL if not supported
*
* Since: 1.8.9
**/
gchar *
fu_version_from_uint24(guint32 val, FwupdVersionFormat kind)
{
if (kind == FWUPD_VERSION_FORMAT_TRIPLET) {
/* BB.CC.DD */
return g_strdup_printf("%u.%u.%u",
(val >> 24) & 0xff,
(val >> 16) & 0xff,
val & 0xffff);
}
if (kind == FWUPD_VERSION_FORMAT_PAIR) {
/* BB.CCDD */
return g_strdup_printf("%u.%u", (val >> 16) & 0xff, val & 0xffff);
}
if (kind == FWUPD_VERSION_FORMAT_NUMBER || kind == FWUPD_VERSION_FORMAT_PLAIN) {
/* BBCCDD */
return g_strdup_printf("%" G_GUINT32_FORMAT, val);
}
if (kind == FWUPD_VERSION_FORMAT_HEX) {
/* 0xBBCCDD */
return g_strdup_printf("0x%06x", val);
}
g_critical("failed to convert version format %s: %u",
fwupd_version_format_to_string(kind),
val);
return NULL;
}
/**
* fu_version_from_uint16:
* @val: a uint16le version number
* @kind: version kind used for formatting, e.g. %FWUPD_VERSION_FORMAT_TRIPLET
*
* Returns a dotted decimal version string from a 16 bit number.
*
* Returns: a version number, e.g. `1.3`, or %NULL if not supported
*
* Since: 1.8.2
**/
gchar *
fu_version_from_uint16(guint16 val, FwupdVersionFormat kind)
{
if (kind == FWUPD_VERSION_FORMAT_BCD) {
return g_strdup_printf("%i.%i",
FU_COMMON_VERSION_DECODE_BCD(val >> 8),
FU_COMMON_VERSION_DECODE_BCD(val));
}
if (kind == FWUPD_VERSION_FORMAT_PAIR) {
return g_strdup_printf("%u.%u", (guint)(val >> 8) & 0xff, (guint)val & 0xff);
}
if (kind == FWUPD_VERSION_FORMAT_NUMBER || kind == FWUPD_VERSION_FORMAT_PLAIN) {
return g_strdup_printf("%" G_GUINT16_FORMAT, val);
}
if (kind == FWUPD_VERSION_FORMAT_HEX) {
/* 0xAABB */
return g_strdup_printf("0x%04x", val);
}
g_critical("failed to convert version format %s: %u",
fwupd_version_format_to_string(kind),
val);
return NULL;
}
static gint
fu_version_compare_char(gchar chr1, gchar chr2)
{
if (chr1 == chr2)
return 0;
if (chr1 == '~')
return -1;
if (chr2 == '~')
return 1;
return chr1 < chr2 ? -1 : 1;
}
static gint
fu_version_compare_chunk(const gchar *str1, const gchar *str2)
{
guint i;
/* trivial */
if (g_strcmp0(str1, str2) == 0)
return 0;
if (str1 == NULL)
return 1;
if (str2 == NULL)
return -1;
/* check each char of the chunk */
for (i = 0; str1[i] != '\0' && str2[i] != '\0'; i++) {
gint rc = fu_version_compare_char(str1[i], str2[i]);
if (rc != 0)
return rc;
}
return fu_version_compare_char(str1[i], str2[i]);
}
static gboolean
_g_ascii_is_digits(const gchar *str)
{
g_return_val_if_fail(str != NULL, FALSE);
for (gsize i = 0; str[i] != '\0'; i++) {
if (!g_ascii_isdigit(str[i]))
return FALSE;
}
return TRUE;
}
static guint
fu_version_format_number_sections(FwupdVersionFormat fmt)
{
if (fmt == FWUPD_VERSION_FORMAT_PLAIN || fmt == FWUPD_VERSION_FORMAT_NUMBER ||
fmt == FWUPD_VERSION_FORMAT_HEX)
return 1;
if (fmt == FWUPD_VERSION_FORMAT_PAIR || fmt == FWUPD_VERSION_FORMAT_BCD)
return 2;
if (fmt == FWUPD_VERSION_FORMAT_TRIPLET || fmt == FWUPD_VERSION_FORMAT_SURFACE_LEGACY ||
fmt == FWUPD_VERSION_FORMAT_SURFACE || fmt == FWUPD_VERSION_FORMAT_DELL_BIOS)
return 3;
if (fmt == FWUPD_VERSION_FORMAT_QUAD || fmt == FWUPD_VERSION_FORMAT_INTEL_ME ||
fmt == FWUPD_VERSION_FORMAT_INTEL_ME2)
return 4;
return 0;
}
/**
* fu_version_ensure_semver:
* @version: (nullable): a version number, e.g. ` V1.2.3 `
* @fmt: a version format, e.g. %FWUPD_VERSION_FORMAT_TRIPLET
*
* Builds a semver from the possibly crazy version number. Depending on the @semver value
* the string will be split and a string in the correct format will be returned.
*
* Returns: a version number, e.g. `1.2.3`, or %NULL if the version was not valid
*
* Since: 1.8.2
*/
gchar *
fu_version_ensure_semver(const gchar *version, FwupdVersionFormat fmt)
{
guint sections_actual;
guint sections_expected = fu_version_format_number_sections(fmt);
g_autofree gchar *tmp = NULL;
g_auto(GStrv) split = NULL;
g_autoptr(GString) str = g_string_new(NULL);
/* split into all sections */
tmp = fu_common_version_ensure_semver(version);
if (tmp == NULL)
return NULL;
if (fmt == FWUPD_VERSION_FORMAT_UNKNOWN)
return g_steal_pointer(&tmp);
split = g_strsplit(tmp, ".", -1);
sections_actual = g_strv_length(split);
/* add zero sections as required */
if (sections_actual < sections_expected) {
for (guint i = 0; i < sections_expected - sections_actual; i++) {
if (str->len > 0)
g_string_append(str, ".");
g_string_append(str, "0");
}
}
/* only add enough sections for the format */
for (guint i = 0; i < sections_actual && i < sections_expected; i++) {
if (str->len > 0)
g_string_append(str, ".");
g_string_append(str, split[i]);
}
/* success */
return g_string_free(g_steal_pointer(&str), FALSE);
}
/**
* fu_common_version_ensure_semver:
* @version: (nullable): a version number, e.g. ` V1.2.3 `
*
* Builds a semver from the possibly crazy version number.
*
* Returns: a version number, e.g. `1.2.3`, or %NULL if the version was not valid
*/
gchar *
fu_common_version_ensure_semver(const gchar *version)
{
gboolean dot_valid = FALSE;
guint digit_cnt = 0;
g_autoptr(GString) version_safe = g_string_new(NULL);
/* invalid */
if (version == NULL)
return NULL;
/* hex prefix */
if (g_str_has_prefix(version, "0x")) {
return fu_version_parse_from_format(version, FWUPD_VERSION_FORMAT_TRIPLET);
}
/* make sane */
for (guint i = 0; version[i] != '\0'; i++) {
if (g_ascii_isdigit(version[i])) {
g_string_append_c(version_safe, version[i]);
digit_cnt++;
dot_valid = TRUE;
continue;
}
if (version[i] == '-' || version[i] == '~') {
g_string_append_c(version_safe, '.');
dot_valid = FALSE;
continue;
}
if (version[i] == '.' && dot_valid && version[i + 1] != '\0') {
g_string_append_c(version_safe, version[i]);
dot_valid = FALSE;
continue;
}
}
/* remove any trailing dot */
if (version_safe->len > 0 && version_safe->str[version_safe->len - 1] == '.')
g_string_truncate(version_safe, version_safe->len - 1);
/* found no digits */
if (digit_cnt == 0)
return NULL;
return g_string_free(g_steal_pointer(&version_safe), FALSE);
}
/**
* fu_version_parse_from_format
* @version: (nullable): a version number
* @fmt: a version format, e.g. %FWUPD_VERSION_FORMAT_TRIPLET
*
* Returns a dotted decimal version string from a version string using @fmt.
* The supported formats are:
*
* - Dotted decimal, e.g. `1.2.3`
* - Base 16, a hex number *with* a 0x prefix, e.g. `0x10203`
* - Base 10, a string containing just [0-9], e.g. `66051`
* - Date in YYYYMMDD format, e.g. `20150915`
*
* Anything with a `.` or that doesn't match `[0-9]` or `0x[a-f,0-9]` is considered
* a string and returned without modification.
*
* Returns: a version number, e.g. `1.0.3`, or %NULL on error
*
* Since: 1.8.2
*/
gchar *
fu_version_parse_from_format(const gchar *version, FwupdVersionFormat fmt)
{
const gchar *version_noprefix = version;
gchar *endptr = NULL;
guint64 tmp;
guint base;
/* sanity check */
if (version == NULL)
return NULL;
/* already dotted decimal */
if (g_strstr_len(version, -1, ".") != NULL)
return g_strdup(version);
/* is a date */
if (g_str_has_prefix(version, "20") && strlen(version) == 8)
return g_strdup(version);
/* convert 0x prefixed strings to dotted decimal */
if (g_str_has_prefix(version, "0x")) {
version_noprefix += 2;
base = 16;
} else {
/* for non-numeric content, just return the string */
if (!_g_ascii_is_digits(version))
return g_strdup(version);
base = 10;
}
/* convert */
tmp = g_ascii_strtoull(version_noprefix, &endptr, base);
if (endptr != NULL && endptr[0] != '\0')
return g_strdup(version);
if (tmp == 0)
return g_strdup(version);
return fu_version_from_uint32((guint32)tmp, fmt);
}
/**
* fu_version_guess_format:
* @version: (nullable): a version number, e.g. `1.2.3`
*
* Guesses the version format from the version number. This is only a heuristic
* and plugins and components should explicitly set the version format whenever
* possible.
*
* If the version format cannot be guessed with any degree of accuracy, the
* %FWUPD_VERSION_FORMAT_UNKNOWN constant is returned.
*
* Returns: a version format, e.g. %FWUPD_VERSION_FORMAT_QUAD
*
* Since: 1.8.2
*/
FwupdVersionFormat
fu_version_guess_format(const gchar *version)
{
guint sz;
g_auto(GStrv) split = NULL;
/* nothing to use */
if (version == NULL || version[0] == '\0')
return FWUPD_VERSION_FORMAT_UNKNOWN;
/* no dots, assume just text */
split = g_strsplit(version, ".", -1);
sz = g_strv_length(split);
if (sz == 1) {
if (g_str_has_prefix(version, "0x") || _g_ascii_is_digits(version))
return FWUPD_VERSION_FORMAT_NUMBER;
return FWUPD_VERSION_FORMAT_PLAIN;
}
/* check for only-digit semver version */
for (guint i = 0; split[i] != NULL; i++) {
/* check sections are plain numbers */
if (!_g_ascii_is_digits(split[i]))
return FWUPD_VERSION_FORMAT_PLAIN;
}
/* the most common formats */
if (sz == 2)
return FWUPD_VERSION_FORMAT_PAIR;
if (sz == 3)
return FWUPD_VERSION_FORMAT_TRIPLET;
if (sz == 4)
return FWUPD_VERSION_FORMAT_QUAD;
/* unknown! */
return FWUPD_VERSION_FORMAT_UNKNOWN;
}
static FwupdVersionFormat
fu_version_format_convert_base(FwupdVersionFormat fmt)
{
if (fmt == FWUPD_VERSION_FORMAT_INTEL_ME || fmt == FWUPD_VERSION_FORMAT_INTEL_ME2)
return FWUPD_VERSION_FORMAT_QUAD;
if (fmt == FWUPD_VERSION_FORMAT_DELL_BIOS)
return FWUPD_VERSION_FORMAT_TRIPLET;
if (fmt == FWUPD_VERSION_FORMAT_BCD)
return FWUPD_VERSION_FORMAT_PAIR;
if (fmt == FWUPD_VERSION_FORMAT_HEX)
return FWUPD_VERSION_FORMAT_NUMBER;
return fmt;
}
/**
* fu_version_verify_format:
* @version: (not nullable): a string, e.g. `0x1234`
* @fmt: a version format
* @error: (nullable): optional return location for an error
*
* Verifies if a version matches the input format.
*
* Returns: TRUE or FALSE
*
* Since: 1.8.2
**/
gboolean
fu_version_verify_format(const gchar *version, FwupdVersionFormat fmt, GError **error)
{
FwupdVersionFormat fmt_base = fu_version_format_convert_base(fmt);
FwupdVersionFormat fmt_guess;
g_return_val_if_fail(version != NULL, FALSE);
g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
/* don't touch */
if (fmt == FWUPD_VERSION_FORMAT_PLAIN)
return TRUE;
/* nothing we can check for */
if (fmt == FWUPD_VERSION_FORMAT_UNKNOWN)
return TRUE;
/* check the base format */
fmt_guess = fu_version_guess_format(version);
if (fmt_guess != fmt_base) {
g_set_error(error,
G_IO_ERROR,
G_IO_ERROR_INVALID_DATA,
"%s is not a valid %s (guessed %s)",
version,
fwupd_version_format_to_string(fmt),
fwupd_version_format_to_string(fmt_guess));
return FALSE;
}
return TRUE;
}
static gint
fu_version_compare_safe(const gchar *version_a, const gchar *version_b)
{
guint longest_split;
g_auto(GStrv) split_a = NULL;
g_auto(GStrv) split_b = NULL;
/* sanity check */
if (version_a == NULL || version_b == NULL)
return G_MAXINT;
/* optimization */
if (g_strcmp0(version_a, version_b) == 0)
return 0;
/* split into sections, and try to parse */
split_a = g_strsplit(version_a, ".", -1);
split_b = g_strsplit(version_b, ".", -1);
longest_split = MAX(g_strv_length(split_a), g_strv_length(split_b));
for (guint i = 0; i < longest_split; i++) {
gchar *endptr_a = NULL;
gchar *endptr_b = NULL;
gint64 ver_a;
gint64 ver_b;
/* we lost or gained a dot */
if (split_a[i] == NULL)
return -1;
if (split_b[i] == NULL)
return 1;
/* compare integers */
ver_a = g_ascii_strtoll(split_a[i], &endptr_a, 10);
ver_b = g_ascii_strtoll(split_b[i], &endptr_b, 10);
if (ver_a < ver_b)
return -1;
if (ver_a > ver_b)
return 1;
/* compare strings */
if ((endptr_a != NULL && endptr_a[0] != '\0') ||
(endptr_b != NULL && endptr_b[0] != '\0')) {
gint rc = fu_version_compare_chunk(endptr_a, endptr_b);
if (rc < 0)
return -1;
if (rc > 0)
return 1;
}
}
/* we really shouldn't get here */
return 0;
}
/**
* fu_version_compare:
* @version_a: (nullable): the semver release version, e.g. `1.2.3`
* @version_b: (nullable): the semver release version, e.g. `1.2.3.1`
* @fmt: a version format, e.g. %FWUPD_VERSION_FORMAT_PLAIN
*
* Compares version numbers for sorting taking into account the version format
* if required.
*
* Returns: -1 if a < b, +1 if a > b, 0 if they are equal, and %G_MAXINT on error
*
* Since: 1.8.2
*/
gint
fu_version_compare(const gchar *version_a, const gchar *version_b, FwupdVersionFormat fmt)
{
if (fmt == FWUPD_VERSION_FORMAT_PLAIN)
return g_strcmp0(version_a, version_b);
if (fmt == FWUPD_VERSION_FORMAT_HEX) {
g_autofree gchar *hex_a = NULL;
g_autofree gchar *hex_b = NULL;
hex_a = fu_version_parse_from_format(version_a, fmt);
hex_b = fu_version_parse_from_format(version_b, fmt);
return fu_version_compare_safe(hex_a, hex_b);
}
return fu_version_compare_safe(version_a, version_b);
}