/* SPDX-License-Identifier: BSD-2-Clause */
/*
 * dhcpcd - DHCP client daemon
 * Copyright (c) 2006-2019 Roy Marples <roy@marples.name>
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */

#ifdef LIBUDEV_NOINIT
#  define LIBUDEV_I_KNOW_THE_API_IS_SUBJECT_TO_CHANGE
#  warning This version of udev is too old does not support
#  warning per device initialization checks.
#  warning As such, dhcpcd will need to depend on the
#  warning udev-settle service or similar if starting
#  warning in master mode.
#endif

#include <libudev.h>
#include <string.h>

#include "../common.h"
#include "../dev.h"
#include "../logerr.h"

static const char udev_name[] = "udev";
static struct udev *udev;
static struct udev_monitor *monitor;

static struct dev_dhcpcd dhcpcd;

static int
udev_listening(void)
{

	return monitor ? 1 : 0;
}

static int
udev_initialized(const char *ifname)
{
	struct udev_device *device;
	int r;

	device = udev_device_new_from_subsystem_sysname(udev, "net", ifname);
	if (device) {
#ifndef LIBUDEV_NOINIT
		r = udev_device_get_is_initialized(device);
#else
		r = 1;
#endif
		udev_device_unref(device);
	} else
		r = 0;
	return r;
}

static int
udev_handle_device(void *ctx)
{
	struct udev_device *device;
	const char *subsystem, *ifname, *action;

	device = udev_monitor_receive_device(monitor);
	if (device == NULL) {
		logerrx("libudev: received NULL device");
		return -1;
	}

	subsystem = udev_device_get_subsystem(device);
	ifname = udev_device_get_sysname(device);
	action = udev_device_get_action(device);

	/* udev filter documentation says "usually" so double check */
	if (strcmp(subsystem, "net") == 0) {
		logdebugx("%s: libudev: %s", ifname, action);
		if (strcmp(action, "add") == 0 || strcmp(action, "move") == 0)
			dhcpcd.handle_interface(ctx, 1, ifname);
		else if (strcmp(action, "remove") == 0)
			dhcpcd.handle_interface(ctx, -1, ifname);
	}

	udev_device_unref(device);
	return 1;
}

static void
udev_stop(void)
{

	if (monitor) {
		udev_monitor_unref(monitor);
		monitor = NULL;
	}

	if (udev) {
		udev_unref(udev);
		udev = NULL;
	}
}

static int
udev_start(void)
{
	int fd;

	if (udev) {
		logerrx("udev: already started");
		return -1;
	}

	logdebugx("udev: starting");
	udev = udev_new();
	if (udev == NULL) {
		logerr("udev_new");
		return -1;
	}
	monitor = udev_monitor_new_from_netlink(udev, "udev");
	if (monitor == NULL) {
		logerr("udev_monitor_new_from_netlink");
		goto bad;
	}
#ifndef LIBUDEV_NOFILTER
	if (udev_monitor_filter_add_match_subsystem_devtype(monitor,
	    "net", NULL) != 0)
	{
		logerr("udev_monitor_filter_add_match_subsystem_devtype");
		goto bad;
	}
#endif
	if (udev_monitor_enable_receiving(monitor) != 0) {
		logerr("udev_monitor_enable_receiving");
		goto bad;
	}
	fd = udev_monitor_get_fd(monitor);
	if (fd == -1) {
		logerr("udev_monitor_get_fd");
		goto bad;
	}
	return fd;

bad:
	udev_stop();
	return -1;
}

int
dev_init(struct dev *dev, const struct dev_dhcpcd *dev_dhcpcd)
{

	dev->name = udev_name;
	dev->initialized = udev_initialized;
	dev->listening = udev_listening;
	dev->handle_device = udev_handle_device;
	dev->stop = udev_stop;
	dev->start = udev_start;

	dhcpcd = *dev_dhcpcd;

	return 0;
}
