/* SPDX-License-Identifier: LGPL-2.1-or-later */
/*
 * Copyright (C) 2017 Red Hat, Inc.
 */

#include "nm-glib-aux/nm-default-glib-i18n-lib.h"

#include "nm-udev-utils.h"

#include <libudev.h>

struct _NMPUdevClient {
    char **              subsystems;
    GSource *            watch_source;
    struct udev *        udev;
    struct udev_monitor *monitor;
    NMUdevClientEvent    event_handler;
    gpointer             event_user_data;
};

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

gboolean
nm_udev_utils_property_as_boolean(const char *uproperty)
{
    /* taken from g_udev_device_get_property_as_boolean() */

    if (uproperty) {
        if (strcmp(uproperty, "1") == 0 || g_ascii_strcasecmp(uproperty, "true") == 0)
            return TRUE;
    }
    return FALSE;
}

const char *
nm_udev_utils_property_decode(const char *uproperty, char **to_free)
{
    const char *p;
    char *      unescaped = NULL;
    char *      n         = NULL;

    if (!uproperty) {
        *to_free = NULL;
        return NULL;
    }

    p = uproperty;
    while (*p) {
        int a, b;

        if (p[0] == '\\' && p[1] == 'x' && (a = g_ascii_xdigit_value(p[2])) >= 0
            && (b = g_ascii_xdigit_value(p[3])) >= 0 && (a || b)) {
            if (!n) {
                gssize l = p - uproperty;

                unescaped = g_malloc(l + strlen(p) + 1 - 3);
                memcpy(unescaped, uproperty, l);
                n = &unescaped[l];
            }
            *n++ = (a << 4) | b;
            p += 4;
        } else {
            if (n)
                *n++ = *p;
            p++;
        }
    }

    if (!n) {
        *to_free = NULL;
        return uproperty;
    }

    *n++ = '\0';
    return (*to_free = unescaped);
}

char *
nm_udev_utils_property_decode_cp(const char *uproperty)
{
    char *cpy;

    uproperty = nm_udev_utils_property_decode(uproperty, &cpy);
    return cpy ?: g_strdup(uproperty);
}

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

static void
_subsystem_split(const char * subsystem_full,
                 const char **out_subsystem,
                 const char **out_devtype,
                 char **      to_free)
{
    char *tmp, *s;

    nm_assert(subsystem_full);
    nm_assert(out_subsystem);
    nm_assert(out_devtype);
    nm_assert(to_free);

    s = strstr(subsystem_full, "/");
    if (s) {
        tmp            = g_strdup(subsystem_full);
        s              = &tmp[s - subsystem_full];
        *s             = '\0';
        *out_subsystem = tmp;
        *out_devtype   = &s[1];
        *to_free       = tmp;
    } else {
        *out_subsystem = subsystem_full;
        *out_devtype   = NULL;
        *to_free       = NULL;
    }
}

static struct udev_enumerate *
nm_udev_utils_enumerate(struct udev *uclient, const char *const *subsystems)
{
    struct udev_enumerate *enumerate;
    guint                  n;

    enumerate = udev_enumerate_new(uclient);

    if (subsystems) {
        for (n = 0; subsystems[n]; n++) {
            const char *  subsystem;
            const char *  devtype;
            gs_free char *to_free = NULL;

            _subsystem_split(subsystems[n], &subsystem, &devtype, &to_free);

            udev_enumerate_add_match_subsystem(enumerate, subsystem);

            if (devtype != NULL)
                udev_enumerate_add_match_property(enumerate, "DEVTYPE", devtype);
        }
    }

    return enumerate;
}

struct udev *
nm_udev_client_get_udev(NMUdevClient *self)
{
    g_return_val_if_fail(self, NULL);

    return self->udev;
}

struct udev_enumerate *
nm_udev_client_enumerate_new(NMUdevClient *self)
{
    g_return_val_if_fail(self, NULL);

    return nm_udev_utils_enumerate(self->udev, (const char *const *) self->subsystems);
}

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

static gboolean
monitor_event(int fd, GIOCondition condition, gpointer user_data)
{
    NMUdevClient *      self = user_data;
    struct udev_device *udevice;

    if (!self->monitor)
        goto out;

    udevice = udev_monitor_receive_device(self->monitor);
    if (udevice == NULL)
        goto out;

    self->event_handler(self, udevice, self->event_user_data);
    udev_device_unref(udevice);

out:
    return TRUE;
}

/**
 * nm_udev_client_new:
 * @subsystems: the subsystems
 * @event_handler: callback for events
 * @event_user_data: user-data for @event_handler
 *
 * Basically, it is g_udev_client_new(), and most notably
 * g_udev_client_constructed().
 *
 * Returns: a new NMUdevClient instance.
 */
NMUdevClient *
nm_udev_client_new(const char *const *subsystems,
                   NMUdevClientEvent  event_handler,
                   gpointer           event_user_data)
{
    NMUdevClient *self;
    guint         n;

    self = g_slice_new0(NMUdevClient);

    self->event_handler   = event_handler;
    self->event_user_data = event_user_data;
    self->subsystems      = subsystems && subsystems[0] ? g_strdupv((char **) subsystems) : NULL;

    self->udev = udev_new();
    if (!self->udev)
        goto fail;

    /* connect to event source */
    if (self->event_handler) {
        self->monitor = udev_monitor_new_from_netlink(self->udev, "udev");
        if (!self->monitor)
            goto fail;

        if (self->subsystems) {
            /* install subsystem filters to only wake up for certain events */
            for (n = 0; self->subsystems[n]; n++) {
                gs_free char *to_free = NULL;
                const char *  subsystem;
                const char *  devtype;

                _subsystem_split(self->subsystems[n], &subsystem, &devtype, &to_free);
                udev_monitor_filter_add_match_subsystem_devtype(self->monitor, subsystem, devtype);
            }

            /* listen to events, and buffer them */
            udev_monitor_set_receive_buffer_size(self->monitor, 4 * 1024 * 1024);
            udev_monitor_enable_receiving(self->monitor);

            self->watch_source = nm_g_unix_fd_source_new(udev_monitor_get_fd(self->monitor),
                                                         G_IO_IN,
                                                         G_PRIORITY_DEFAULT,
                                                         monitor_event,
                                                         self,
                                                         NULL);
            g_source_attach(self->watch_source, g_main_context_get_thread_default());
        }
    }

    return self;

fail:
    return nm_udev_client_destroy(self);
}

NMUdevClient *
nm_udev_client_destroy(NMUdevClient *self)
{
    if (!self)
        return NULL;

    nm_clear_g_source_inst(&self->watch_source);

    udev_monitor_unref(self->monitor);
    self->monitor = NULL;
    udev_unref(self->udev);
    self->udev = NULL;

    g_strfreev(self->subsystems);

    g_slice_free(NMUdevClient, self);

    return NULL;
}
