/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- * * Copyright (C) 2020-2022 Matthias Klumpp * Copyright (C) 2010 Julian Andres Klode * * 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, see . */ #include "as-vercmp.h" #include #include #include "as-macros.h" /** * SECTION:as-vercmp * @short_description: Version comparison functions. * @include: appstream.h * * Compare software version numbers. */ typedef struct AsVersion { const gchar *epoch; const gchar *version; const gchar *version_end; const gchar *revision; const gchar *revision_end; } AsVersion; /** * as_version_parse: */ static void as_version_parse (AsVersion *version, const gchar *v) { const gchar *epoch_end = strchr (v, ':'); const gchar *version_end = strrchr (v, '-'); const gchar *complete_end = v + strlen (v); version->epoch = (epoch_end == NULL) ? "" : v; version->version = (epoch_end == NULL) ? v : epoch_end + 1; version->version_end = (version_end == NULL) ? complete_end : version_end; version->revision = (version_end == NULL) ? "0" : version_end + 1; version->revision_end = ((version_end == NULL) ? version->revision + 1 : complete_end); } /** * cmp_number: * @a: The first number as a string. * @b: The second number as a string. * @pa: A pointer to a pointer which will be set to the first non-digit in @a. * @pb: A pointer to a pointer which will be set to the first non-digit in @b. * * Compares two numbers that are represented as strings * against each other. Compared to converting to an int * and comparing the integers, this has the advantage * that it does not cause overflow. */ static gint cmp_number (const gchar *a, const gchar *b, const gchar **pa, const gchar **pb) { gint res = 0; if (*a == '\0' && *b == '\0') return 0; for (; *a == '0'; a++) ; for (; *b == '0'; b++) ; for (; g_ascii_isdigit (*a) && g_ascii_isdigit (*b); a++, b++) { if (res == 0 && *a != *b) res = *a < *b ? -1 : 1; } if (g_ascii_isdigit (*a)) { if (!g_ascii_isdigit (*b)) res = 1; } else if (g_ascii_isdigit (*b)) { res = -1; } if (pa != NULL) { g_assert (pb != NULL); *pa = a; *pb = b; } return res; } /** * cmp_part: */ static gint cmp_part (const gchar *a, const gchar *a_end, const gchar *b, const gchar *b_end) { while (a != a_end || b != b_end) { int cmpres; for (; !g_ascii_isdigit (*a) || !g_ascii_isdigit (*b); a++, b++) { if (a == a_end && b == b_end) return 0; else if (*a == *b && a != a_end && b != b_end) continue; /* Tilde always sorts first; i.e., the string with tilde loses */ else if (*a == '~' || *b == '~') return (*a == '~') ? -1 : 1; /* One string is empty, other is a number -> go into number mode */ else if ((a == a_end && *b == '0') || (b == b_end && *a == '0')) return cmp_number (a, b, NULL, NULL); /* One string is empty, other is not a number -> other wins */ else if (a == a_end || b == b_end) return (a == a_end) ? -1 : 1; /* One non-digit part is shorter than the other one */ else if (g_ascii_isdigit (*a) != g_ascii_isdigit (*b)) return g_ascii_isdigit (*a) ? -1 : 1; /* Alpha looses against not alpha */ else if (g_ascii_isalpha (*a) != g_ascii_isalpha (*b)) return g_ascii_isalpha (*a) ? -1 : 1; /* Standard ASCII comparison */ else return *a < *b ? -1 : 1; } /* Now compare numbers */ cmpres = cmp_number (a, b, &a, &b); if (cmpres != 0 || (a == a_end && b == b_end)) return cmpres; } return 0; } /** * as_vercmp: * @a: First version number * @b: Second version number * @flags: Flags, e.g. %AS_VERCMP_FLAG_NONE * * Compare alpha and numeric segments of two software versions, * considering @flags. * * Returns: >>0 if a is newer than b; * 0 if a and b are the same version; * <<0 if b is newer than a */ gint as_vercmp (const gchar *a, const gchar *b, AsVercmpFlags flags) { AsVersion ver_a, ver_b; gint res = 0; if (a == 0 && b == 0) return 0; if (a == NULL) return -1; if (b == NULL) return 1; /* check if a and b are the same pointer */ if (a == b) return 0; /* Optimize the case of differing single digit epochs. */ if (!as_flags_contains (flags, AS_VERCMP_FLAG_IGNORE_EPOCH)) { if (*a != *b && *a != '\0' && *b != '\0' && a[1] == ':' && b[1] == ':') return *a < *b ? -1 : 1; } /* easy comparison to see if versions are identical */ if (g_strcmp0 (a, b) == 0) return 0; as_version_parse (&ver_a, a); as_version_parse (&ver_b, b); if (G_UNLIKELY (ver_a.epoch != ver_b.epoch) && (!as_flags_contains (flags, AS_VERCMP_FLAG_IGNORE_EPOCH)) && (res = cmp_number (ver_a.epoch, ver_b.epoch, NULL, NULL)) != 0) goto out; else if ((res = cmp_part (ver_a.version, ver_a.version_end, ver_b.version, ver_b.version_end)) != 0) goto out; /* Optimizes for native version numbers (where revision is a string literal) */ else if (G_LIKELY (ver_a.revision != ver_b.revision) && (res = cmp_part (ver_a.revision, ver_a.revision_end, ver_b.revision, ver_b.revision_end)) != 0) goto out; out: return res; } /** * as_vercmp_simple: * @a: First version number * @b: Second version number * * Compare alpha and numeric segments of two software versions. * * Returns: >>0 if a is newer than b; * 0 if a and b are the same version; * <<0 if b is newer than a */ gint as_vercmp_simple (const gchar *a, const gchar *b) { return as_vercmp (a, b, AS_VERCMP_FLAG_NONE); } /** * as_vercmp_test_match: * @ver1: first version number * @compare: the comparison operator * @ver2: second version number * @flags: the #AsVercmpFlags to use * * Compare two version numbers and check if the given version comparator matches. * * Returns: %TRUE if the version comparison matches, %FALSE otherwise. * * Since: 0.16.0 */ gboolean as_vercmp_test_match (const gchar *ver1, AsRelationCompare compare, const gchar *ver2, AsVercmpFlags flags) { gint rc; g_return_val_if_fail (compare != AS_RELATION_COMPARE_UNKNOWN, FALSE); rc = as_vercmp (ver1, ver2, flags); switch (compare) { case AS_RELATION_COMPARE_EQ: return rc == 0; case AS_RELATION_COMPARE_NE: return rc != 0; case AS_RELATION_COMPARE_LT: return rc < 0; case AS_RELATION_COMPARE_GT: return rc > 0; case AS_RELATION_COMPARE_LE: return rc <= 0; case AS_RELATION_COMPARE_GE: return rc >= 0; default: return FALSE; } }