/* SPDX-License-Identifier: LGPL-2.1-or-later */

#include "libnm-glib-aux/nm-default-glib.h"

#include "nm-uuid.h"

#include "libnm-glib-aux/nm-random-utils.h"
#include "libnm-glib-aux/nm-str-buf.h"

/*****************************************************************************/

const NMUuid nm_uuid_ns_zero =
    NM_UUID_INIT(00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00);

/* arbitrarily chosen namespace UUID for some uses of nm_uuid_generate_from_strings_old().
 * Try not to re-use this namespace, instead, generate a unique one. */
const NMUuid nm_uuid_ns_1 =
    NM_UUID_INIT(b4, 25, e9, fb, 75, 98, 44, b4, 9e, 3b, 5a, 2e, 3a, aa, 49, 05);

char *
nm_uuid_unparse_case(const NMUuid *uuid, char out_str[static 37], gboolean upper_case)
{
    char *s;
    int   i;

    nm_assert(uuid);
    nm_assert(out_str);

    s = out_str;
    for (i = 0; i < 16; i++) {
        const guint8 c = uuid->uuid[i];

        if (NM_IN_SET(i, 4, 6, 8, 10))
            *(s++) = '-';
        *(s++) = nm_hexchar(c >> 4, upper_case);
        *(s++) = nm_hexchar(c, upper_case);
    }
    *s = '\0';

    return out_str;
}

/*****************************************************************************/

gboolean
nm_uuid_parse_full(const char *str, NMUuid *out_uuid, gboolean *out_is_normalized)
{
    NMUuid   uuid;
    guint8  *p;
    int      i;
    gboolean is_normalized = TRUE;

    nm_assert(str);

    p = uuid.uuid;

    for (i = 0; TRUE;) {
        int v0;
        int v1;

        if (NM_IN_SET(i, 8, 13, 18, 23)) {
            if (str[i] != '-')
                return FALSE;
            i++;
            continue;
        }

        if (i == 36) {
            if (str[i] != '\0')
                return FALSE;

            NM_SET_OUT(out_is_normalized, is_normalized);
            NM_SET_OUT(out_uuid, uuid);
            return TRUE;
        }

#define _hexchar(ch, out_is_normalized2)                                    \
    ({                                                                      \
        const char _ch     = (ch);                                          \
        int        _result = -1;                                            \
                                                                            \
        if (_ch >= '0') {                                                   \
            if (_ch <= '9')                                                 \
                _result = (_ch - '0');                                      \
            else if (_ch >= 'A') {                                          \
                if (_ch <= 'F') {                                           \
                    *(out_is_normalized2) = FALSE;                          \
                    _result               = ((int) _ch) + (10 - (int) 'A'); \
                } else if (_ch >= 'a' && _ch <= 'f')                        \
                    _result = ((int) _ch) + (10 - (int) 'a');               \
            }                                                               \
        }                                                                   \
                                                                            \
        _result;                                                            \
    })

        v0 = _hexchar(str[i++], &is_normalized);
        if (v0 < 0)
            return FALSE;
        v1 = _hexchar(str[i++], &is_normalized);
        if (v1 < 0)
            return FALSE;

        *(p++) = (v0 << 4) + v1;
    }
}

/*****************************************************************************/

NMUuid *
nm_uuid_generate_random(NMUuid *out_uuid)
{
    nm_assert(out_uuid);

    /* https://tools.ietf.org/html/rfc4122#section-4.4 */

    /* See also, systemd's id128_make_v4_uuid() */

    /* nm_random_get_bytes() is supposed to try hard to give good
     * randomness. If it fails, it still makes an effort to fill
     * random data into the buffer. There is not much we can do about
     * that case, except making sure that it does not happen in the
     * first place. */
    nm_random_get_bytes(out_uuid, sizeof(*out_uuid));

    /* Set the four most significant bits (bits 12 through 15) of the
     * time_hi_and_version field to the 4-bit version number from
     * Section 4.1.3. */
    out_uuid->uuid[6] = (out_uuid->uuid[6] & 0x0Fu) | 0x40u;

    /* Set the two most significant bits (bits 6 and 7) of the
     * clock_seq_hi_and_reserved to zero and one, respectively. */
    out_uuid->uuid[8] = (out_uuid->uuid[8] & 0x3Fu) | 0x80u;

    return out_uuid;
}

/*****************************************************************************/

gboolean
nm_uuid_is_normalized_full(const char *str)
{
    /* The only reason why this exists is that nm_uuid_is_normalized() is an inline function.
     * If you need to forward declare the function, that won't work.
     *
     * Usually, you wouldn't use this variant! */
    return nm_uuid_is_normalized(str);
}

/*****************************************************************************/

/**
 * nm_uuid_is_valid_nmlegacy()
 * @str: the string to check whether it's a valid UUID.
 *
 * Note that this does not perform a strict check.
 * Instead, it checks a more relaxed format (including
 * non valid UUID strings). This is for backward compatibility,
 * where older code did not perform a strict check.
 *
 * Returns: %TRUE, if the string is a valid legacy format.
 *   This may not be a valid UUID!
 */
gboolean
nm_uuid_is_valid_nmlegacy(const char *str)
{
    const char *p          = str;
    int         num_dashes = 0;

    if (!p)
        return FALSE;

    while (*p) {
        if (*p == '-')
            num_dashes++;
        else if (!g_ascii_isxdigit(*p))
            return FALSE;
        p++;
    }

    /* While we accept here bogus strings as UUIDs, they must contain only
     * hexdigits and '-', and they must be either 36 or 40 chars long. */

    if ((num_dashes == 4) && (p - str == 36))
        return TRUE;

    /* Backwards compat for older configurations */
    if ((num_dashes == 0) && (p - str == 40))
        return TRUE;

    return FALSE;
}

/*****************************************************************************/

gboolean
nm_uuid_is_valid_nm(const char *str,
                    gboolean   *out_normalized,
                    char       *out_normalized_str /* [static 37] */)
{
    NMUuid   uuid;
    gboolean is_normalized;

    /* @out_normalized_str is only set, if normalization was necessary
     * and possible. The caller cannot request @out_normalized_str, without
     * also requesting @out_normalized. Otherwise, they couldn't know whether
     * a normalized string was returned. */
    nm_assert(!out_normalized_str || out_normalized);

    if (!str)
        return FALSE;

    if (nm_uuid_parse_full(str, &uuid, &is_normalized)) {
        if (is_normalized) {
            /* @str is already normalized. No need to normalize again, so
             * @out_normalized is FALSE. */
            NM_SET_OUT(out_normalized, FALSE);
        } else {
            NM_SET_OUT(out_normalized, TRUE);
            if (out_normalized_str) {
                /* we need to normalize the UUID */
                nm_uuid_unparse(&uuid, out_normalized_str);
            }
        }
        return TRUE;
    }

    if (nm_uuid_is_valid_nmlegacy(str)) {
        /* This is not a valid UUID, but something that we used to
         * accept according to nm_uuid_is_valid_nmlegacy().
         *
         * Normalize it by sha1 hashing the string. Upper case characters
         * are made lower case first. */
        NM_SET_OUT(out_normalized, TRUE);
        if (out_normalized_str) {
            char str_lower[40];
            int  i;

            nm_assert(strlen(str) <= G_N_ELEMENTS(str_lower));

            /* normalize first to lower-case. */
            for (i = 0; str[i]; i++) {
                nm_assert(i < G_N_ELEMENTS(str_lower));
                str_lower[i] = g_ascii_tolower(str[i]);
            }
            nm_assert(i <= G_N_ELEMENTS(str_lower));

            /* The namespace UUID is chosen randomly. */
            nm_uuid_generate_from_string(
                &uuid,
                str_lower,
                i,
                NM_UUID_TYPE_VERSION5,
                &NM_UUID_INIT(4e, 72, f7, 09, ca, 95, 44, 05, 90, 53, 1f, 43, 29, 4a, 61, 8c));
            nm_uuid_unparse(&uuid, out_normalized_str);
        }
        return TRUE;
    }

    /* UUID is not valid. */
    return FALSE;
}

/*****************************************************************************/

gboolean
nm_uuid_is_null(const NMUuid *uuid)
{
    int i;

    if (!uuid)
        return TRUE;

    for (i = 0; i < (int) G_N_ELEMENTS(uuid->uuid); i++) {
        if (uuid->uuid[i])
            return FALSE;
    }
    return TRUE;
}

char *
nm_uuid_generate_random_str(char buf[static 37])
{
    NMUuid uuid;

    nm_assert(buf);

    nm_uuid_generate_random(&uuid);
    return nm_uuid_unparse(&uuid, buf);
}

/*****************************************************************************/

/**
 * nm_uuid_generate_from_string:
 * @uuid: the UUID to update inplace. This function cannot
 *   fail to succeed.
 * @s: a string to use as the seed for the UUID
 * @slen: if negative, treat @s as zero terminated C string.
 *   Otherwise, assume the length as given (and allow @s to be
 *   non-null terminated or contain '\0').
 * @uuid_type: a type identifier which UUID format to generate.
 * @type_args: additional arguments, depending on the uuid_type
 *
 * For a given @s, this function will always return the same UUID.
 *
 * Returns: the input @uuid. This function cannot fail.
 **/
NMUuid *
nm_uuid_generate_from_string(NMUuid       *uuid,
                             const char   *s,
                             gssize        slen,
                             NMUuidType    uuid_type,
                             const NMUuid *type_args)
{
    nm_auto_free_checksum GChecksum *sum = NULL;
    union {
        guint8 sha1[NM_UTILS_CHECKSUM_LENGTH_SHA1];
        guint8 md5[NM_UTILS_CHECKSUM_LENGTH_MD5];
        NMUuid uuid;
    } digest;
    gsize         digest_len;
    GChecksumType checksum_type;

    G_STATIC_ASSERT_EXPR(sizeof(digest.md5) >= sizeof(digest.uuid));
    G_STATIC_ASSERT_EXPR(sizeof(digest.sha1) >= sizeof(digest.uuid));

    g_return_val_if_fail(uuid, NULL);
    g_return_val_if_fail(slen <= 0 || s, NULL);

    if (slen < 0)
        slen = s ? strlen(s) : 0;

    switch (uuid_type) {
    case NM_UUID_TYPE_LEGACY:
        nm_assert(!type_args);
        type_args     = NULL;
        checksum_type = G_CHECKSUM_MD5;
        break;
    case NM_UUID_TYPE_VERSION3:
        if (!type_args)
            type_args = &nm_uuid_ns_zero;
        checksum_type = G_CHECKSUM_MD5;
        break;
    case NM_UUID_TYPE_VERSION5:
        if (!type_args)
            type_args = &nm_uuid_ns_zero;
        checksum_type = G_CHECKSUM_SHA1;
        break;
    default:
        g_return_val_if_reached(NULL);
    }

    sum = g_checksum_new(checksum_type);
    if (type_args)
        g_checksum_update(sum, (guchar *) type_args, sizeof(*type_args));
    g_checksum_update(sum, (guchar *) s, slen);

    digest_len = sizeof(digest);
    g_checksum_get_digest(sum, (guint8 *) &digest, &digest_len);

    nm_assert(digest_len >= sizeof(digest.uuid));
    nm_assert(digest_len
              == ((checksum_type == G_CHECKSUM_MD5 ? NM_UTILS_CHECKSUM_LENGTH_MD5
                                                   : NM_UTILS_CHECKSUM_LENGTH_SHA1)));

    *uuid = digest.uuid;

    if (uuid_type != NM_UUID_TYPE_LEGACY) {
        uuid->uuid[6] = (uuid->uuid[6] & 0x0F) | (uuid_type << 4);
        uuid->uuid[8] = (uuid->uuid[8] & 0x3F) | 0x80;
    }

    return uuid;
}

/**
 * nm_uuid_generate_from_string_str:
 * @s: a string to use as the seed for the UUID
 * @slen: if negative, treat @s as zero terminated C string.
 *   Otherwise, assume the length as given (and allow @s to be
 *   non-null terminated or contain '\0').
 * @uuid_type: a type identifier which UUID format to generate.
 * @type_args: additional arguments, depending on the uuid_type
 *
 * For a given @s, this function will always return the same UUID.
 *
 * Returns: a newly allocated UUID suitable for use as the #NMSettingConnection
 * object's #NMSettingConnection:id: property
 **/
char *
nm_uuid_generate_from_string_str(const char   *s,
                                 gssize        slen,
                                 NMUuidType    uuid_type,
                                 const NMUuid *type_args)
{
    NMUuid        uuid;
    const NMUuid *u;

    u = nm_uuid_generate_from_string(&uuid, s, slen, uuid_type, type_args);

    if (G_UNLIKELY(!u))
        return nm_assert_unreachable_val(NULL);
    nm_assert(u == &uuid);

    return nm_uuid_unparse(&uuid, g_new(char, 37));
}

/**
 * nm_uuid_generate_from_strings_strv:
 * @uuid_type: the UUID type to use. Prefer version 5 unless you have
 *   good reasons.
 * @type_args: the namespace UUID.
 * @strv: (nullable): the strv list to hash. Can be NULL, in which
 *   case the result is different from an empty array.
 * @len: if negative, @strv is a NULL terminated array. Otherwise,
 *   it is the length of the strv array. In the latter case it may
 *   also contain NULL strings.
 *
 * Returns a @uuid_type UUID based on the concatenated C strings.
 * It does not simply concatenate them, but also includes the
 * terminating '\0' character. For example "a", "b", gives
 * "a\0b\0".
 * This has the advantage, that the following invocations
 * all give different UUIDs: (NULL), (""), ("",""), ("","a"), ("a",""),
 * ("aa"), ("aa", ""), ("", "aa"), ...
 */
char *
nm_uuid_generate_from_strings_strv(NMUuidType         uuid_type,
                                   const NMUuid      *type_args,
                                   const char *const *strv,
                                   gssize             len)
{
    nm_auto_str_buf NMStrBuf str = NM_STR_BUF_INIT_A(NM_UTILS_GET_NEXT_REALLOC_SIZE_232, TRUE);
    gsize                    slen;
    const char              *s;

    if (len > 0) {
        gboolean has_nulls = FALSE;
        gssize   i;

        nm_assert(len == 0 || strv);

        for (i = 0; i < len; i++) {
            if (strv[i])
                nm_str_buf_append_len(&str, strv[i], strlen(strv[i]) + 1u);
            else
                has_nulls = TRUE;
        }
        if (has_nulls) {
            /* We either support a NULL terminated strv array, or a ptr array of fixed
             * length (@len argument).
             *
             * If there are no NULLs within the first @len strings, then the result
             * is the same. If there are any NULL strings, we need to encode that
             * in a unique way. We do that by appending a bitmap of the elements
             * whether they were set, plus one 'n' character (without NUL termination).
             * None of the other branches below hashes to that, so this will uniquely
             * encoded the NULL strings.
             */
            for (i = 0; i < len; i++)
                nm_str_buf_append_c(&str, strv[i] ? '1' : '_');
            nm_str_buf_append_c(&str, 'n');
        }
        slen = str.len;
        s    = nm_str_buf_get_str_unsafe(&str);
    } else if (!strv) {
        /* NULL is treated differently from an empty strv. We achieve that
         * by using a non-empty, non-NUL terminated string (which cannot happen
         * in the other cases). */
        slen = 1;
        s    = "x";
    } else if (!strv[0] || len == 0) {
        slen = 0;
        s    = "";
    } else if (!strv[1]) {
        slen = strlen(strv[0]) + 1u;
        s    = strv[0];
    } else {
        /* We concatenate the NUL terminated string, including the NUL
         * character. This way, ("a","a"), ("aa"), ("aa","") all hash
         * differently. */
        for (; strv[0]; strv++)
            nm_str_buf_append_len(&str, strv[0], strlen(strv[0]) + 1u);
        slen = str.len;
        s    = nm_str_buf_get_str_unsafe(&str);
    }

    return nm_uuid_generate_from_string_str(s, slen, uuid_type, type_args);
}
