// SPDX-License-Identifier: GPL-2.0-only
/* Copyright (c) 2019-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved */

#ifndef OSI_STRIPPED_LIB
#include "ether_linux.h"
#include <net/udp.h>

/**
 * @brief Ethernet packet context for loopback packet
 */
struct ether_packet_ctxt {
	/** Destination MAC address in Ethernet header */
#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 17, 0)
	const unsigned char *dst;
#else
	unsigned char *dst;
#endif
};

/**
 * @brief Ethernet selftests private data
 */
struct ether_test_priv_data {
	/** Ethernet selftest packet context */
	struct ether_packet_ctxt *ctxt;
	/** Ethernet packet type to handover the packet to Rx handler */
	struct packet_type pt;
	/** Packet reception completion indication */
	struct completion comp;
	/** Indication for loopback packet reception process completed */
	bool completed;
};


/**
 * @brief Ethernet packet test header
 */
struct ether_testhdr {
	/** Ethernet selftest data */
	__be64 magic;
};

/**
 * @addtogroup Ethernet selftest helper macros
 *
 * @brief Helper macros for Ethernet selftests
 * @{
 */
#define ETHER_TEST_PKT_MAGIC	0xdeadcafecafedeadULL
#define ETHER_TEST_PKT_SIZE	(sizeof(struct ethhdr) +\
				 sizeof(struct iphdr) +\
				 sizeof(struct udphdr) +\
				 sizeof(struct ether_testhdr))
/* UDP discard protocol port */
#define ETHER_UDP_TEST_PORT	9
#define ETHER_IP_IHL		5
#define ETHER_IP_TTL		32
/** @} */

/**
 * @brief ether_test_get_udp_skb - Fill socket buffer with UDP packet
 *
 * Algorithm: Fills socket buffer with UDP packet.
 *
 * @param[in] pdata: Ethernet OSD private data
 * @param[in] ctxt: Ethernet packet context
 *
 * @retval skb pointer on success
 * @retval NULL on failure.
 */
static struct sk_buff *ether_test_get_udp_skb(struct ether_priv_data *pdata,
					      struct ether_packet_ctxt *ctxt)
{
	struct sk_buff *skb = NULL;
	struct ether_testhdr *testhdr;
	struct ethhdr *ethh;
	struct udphdr *udph;
	struct iphdr *iph;
	int    iplen;

	skb = netdev_alloc_skb(pdata->ndev, ETHER_TEST_PKT_SIZE);
	if (!skb) {
		netdev_err(pdata->ndev, "Failed to allocate loopback skb\n");
		return NULL;
	}

	skb_reserve(skb, NET_IP_ALIGN);

	/*  Reserve for ethernet and IP header  */
	ethh = skb_push(skb, ETH_HLEN);
	skb_reset_mac_header(skb);

	skb_set_network_header(skb, skb->len);
	iph = skb_put(skb, sizeof(struct iphdr));

	skb_set_transport_header(skb, skb->len);
	udph = skb_put(skb, sizeof(struct udphdr));

	/* Fill ETH header */
	ether_addr_copy(ethh->h_dest, ctxt->dst);
	eth_zero_addr(ethh->h_source);
	ethh->h_proto = htons(ETH_P_IP);

	/* Fill UDP header */
	udph->source = htons(ETHER_UDP_TEST_PORT);
	udph->dest = htons(ETHER_UDP_TEST_PORT); /* Discard Protocol */
	udph->len = htons(sizeof(struct ether_testhdr) + sizeof(struct udphdr));
	udph->check = OSI_NONE;

	/* Fill IP header */
	iph->ihl = ETHER_IP_IHL;
	iph->ttl = ETHER_IP_TTL;
	iph->version = IPVERSION;
	iph->protocol = IPPROTO_UDP;
	iplen = sizeof(struct iphdr) + sizeof(struct udphdr) +
		sizeof(struct ether_testhdr);
	iph->tot_len = htons(iplen);
	iph->frag_off = OSI_NONE;
	iph->saddr = OSI_NONE;
	iph->daddr = OSI_NONE;
	iph->tos = OSI_NONE;
	iph->id = OSI_NONE;
	ip_send_check(iph);

	/* Fill test header and data */
	testhdr = skb_put(skb, sizeof(*testhdr));
	testhdr->magic = cpu_to_be64(ETHER_TEST_PKT_MAGIC);

	skb->csum = OSI_NONE;
	skb->ip_summed = CHECKSUM_PARTIAL;
	udp4_hwcsum(skb, iph->saddr, iph->daddr);
	skb->protocol = htons(ETH_P_IP);
	skb->pkt_type = PACKET_HOST;
	skb->dev = pdata->ndev;

	return skb;
}

/**
 * @brief ether_test_loopback_validate - Loopback packet validation
 *
 * Algorithm: Validated received loopback packet with the packet sent.
 *
 * @param[in] skb: socket buffer pointer
 * @param[in] ndev: Network device pointer
 * @param[in] pt: Packet type for ethernet received packet
 * @param[in] orig_dev: Original network device pointer
 *
 * @retval 0 always
 */
static int ether_test_loopback_validate(struct sk_buff *skb,
					struct net_device *ndev,
					struct packet_type *pt,
					struct net_device *orig_ndev)
{
	struct ether_test_priv_data *tpdata = pt->af_packet_priv;
#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 17, 0)
	const unsigned char *dst = tpdata->ctxt->dst;
#else
	unsigned char *dst = tpdata->ctxt->dst;
#endif
	struct ether_testhdr *thdr;
	struct ethhdr *ehdr;
	struct udphdr *uhdr;
	struct iphdr *ihdr;

	skb = skb_unshare(skb, GFP_ATOMIC);
	if (!skb)
		goto out;

	if (skb_linearize(skb))
		goto out;
	if (skb_headlen(skb) < (ETHER_TEST_PKT_SIZE - ETH_HLEN))
		goto out;

	ehdr = (struct ethhdr *)skb_mac_header(skb);
	if (dst) {
		if (!ether_addr_equal_unaligned(ehdr->h_dest, dst))
			goto out;
	}

	ihdr = ip_hdr(skb);
	if (ihdr->protocol != IPPROTO_UDP)
		goto out;

	uhdr = (struct udphdr *)((u8 *)ihdr + 4 * ihdr->ihl);
	if (uhdr->dest != htons(ETHER_UDP_TEST_PORT))
		goto out;

	thdr = (struct ether_testhdr *)((u8 *)uhdr + sizeof(*uhdr));

	if (thdr->magic != cpu_to_be64(ETHER_TEST_PKT_MAGIC))
		goto out;

	tpdata->completed = true;
	complete(&tpdata->comp);
out:
	kfree_skb(skb);
	return 0;
}

/**
 * @brief ether_test_loopback - Ethernet selftest for loopback
 *
 * Algorithm:
 * 1) It registers Rx handler with network type for specifc packet type.
 * 2) Gets a SKB with UDP/Ethernet headers updated
 * 3) Transmits packet with dev_queue_xmit().
 *
 * @param[in] pdata: Ethernet OSD private data
 * @param[in] ctxt: Ethernet packet context
 *
 * @retval zero on success.
 * @retval negative value on failure.
 */
static int ether_test_loopback(struct ether_priv_data *pdata,
			       struct ether_packet_ctxt *ctxt)
{
	struct ether_test_priv_data *tpdata;
	struct sk_buff *skb = NULL;
	int ret = 0;

	tpdata = kzalloc(sizeof(*tpdata), GFP_KERNEL);
	if (!tpdata)
		return -ENOMEM;

	tpdata->completed = false;
	init_completion(&tpdata->comp);

	tpdata->pt.type = htons(ETH_P_IP);
	tpdata->pt.func = ether_test_loopback_validate;
	tpdata->pt.dev = pdata->ndev;
	tpdata->pt.af_packet_priv = tpdata;
	tpdata->ctxt = ctxt;
	dev_add_pack(&tpdata->pt);

	skb = ether_test_get_udp_skb(pdata, ctxt);
	if (!skb) {
		ret = -ENOMEM;
		goto cleanup;
	}

	skb_set_queue_mapping(skb, 0);
	ret = dev_queue_xmit(skb);
	if (ret)
		goto cleanup;

	if (!wait_for_completion_timeout(&tpdata->comp,
					 msecs_to_jiffies(200))) {
		ret = -ETIMEDOUT;
		goto cleanup;
	}

	ret = !tpdata->completed;
cleanup:
	dev_remove_pack(&tpdata->pt);
	kfree(tpdata);
	return ret;
}

/**
 * @brief ether_test_mac_loopback - Ethernet selftest for MAC loopback
 *
 * @param[in] pdata: Ethernet OSD private data
 *
 * @retval zero on success
 * @retval negative value on failure.
 */
static int ether_test_mac_loopback(struct ether_priv_data *pdata)
{
	struct ether_packet_ctxt ctxt = { };

	ctxt.dst = pdata->ndev->dev_addr;
	return ether_test_loopback(pdata, &ctxt);
}

/**
 * @brief ether_test_phy_loopback - Ethernet selftest for PHY loopback
 *
 * @param[in] pdata: Ethernet OSD private data
 *
 * @retval zero on success
 * @retval negative value on failure.
 */
static int ether_test_phy_loopback(struct ether_priv_data *pdata)
{
	struct ether_packet_ctxt ctxt = { };
	int ret = 0;

	if (!pdata->phydev)
		return -ENODEV;

	ret = phy_loopback(pdata->phydev, true);
	if (ret != 0 && ret != -EBUSY)
		return ret;

	ctxt.dst = pdata->ndev->dev_addr;

	return ether_test_loopback(pdata, &ctxt);
}

/**
 * @brief ether_test_mmc_counters - Ethernet selftest for MMC Counters
 *
 * @param[in] pdata: Ethernet OSD private data
 *
 * @retval zero on success
 * @retval negative value on failure.
 */
static int ether_test_mmc_counters(struct ether_priv_data *pdata)
{
	struct osi_core_priv_data *osi_core = pdata->osi_core;
	struct osi_ioctl ioctl_data = {};
	unsigned int mmc_tx_framecount_g = 0;
	unsigned int mmc_rx_framecount_gb = 0;
	unsigned int mmc_rx_ipv4_gd = 0;
	unsigned int mmc_rx_udp_gd = 0;
	int ret = 0;

	ioctl_data.cmd = OSI_CMD_READ_MMC;
	ret = osi_handle_ioctl(osi_core, &ioctl_data);
	if (ret < 0)
		return ret;

	mmc_tx_framecount_g = osi_core->mmc.mmc_tx_framecount_g;
	mmc_rx_framecount_gb = osi_core->mmc.mmc_rx_framecount_gb;
	mmc_rx_ipv4_gd = osi_core->mmc.mmc_rx_ipv4_gd;
	mmc_rx_udp_gd = osi_core->mmc.mmc_rx_udp_gd;

	ret = ether_test_mac_loopback(pdata);
	if (ret < 0)
		return ret;
	ioctl_data.cmd = OSI_CMD_READ_MMC;
	ret = osi_handle_ioctl(osi_core, &ioctl_data);
	if (ret < 0)
		return ret;

	if (osi_core->mmc.mmc_tx_framecount_g <= mmc_tx_framecount_g)
		return -1;
	if (osi_core->mmc.mmc_rx_framecount_gb <= mmc_rx_framecount_gb)
		return -1;
	if (osi_core->mmc.mmc_rx_ipv4_gd <= mmc_rx_ipv4_gd)
		return -1;
	if (osi_core->mmc.mmc_rx_udp_gd <= mmc_rx_udp_gd)
		return -1;

	return 0;
}

#define ETHER_LOOPBACK_NONE	0
#define ETHER_LOOPBACK_MAC	1
#define ETHER_LOOPBACK_PHY	2

static const struct ether_test {
	char name[ETH_GSTRING_LEN];
	int lb;
	int (*fn)(struct ether_priv_data *pdata);
} ether_selftests[] = {
	{
		.name = "MAC Loopback		",
		.lb = ETHER_LOOPBACK_MAC,
		.fn = ether_test_mac_loopback,
	}, {
		.name = "PHY Loopback		",
		.lb = ETHER_LOOPBACK_PHY,
		.fn = ether_test_phy_loopback,
	}, {
		.name = "MMC Counters		",
		.lb = ETHER_LOOPBACK_MAC,
		.fn = ether_test_mmc_counters,
	},
};

/**
 * @brief ether_selftest_run - Ethernet selftests.
 *
 * @param[in] dev: Network device pointer.
 * @param[in] etest: Ethernet ethtool test pointer.
 * @param[in] buf: Buffer pointer to hold test status.
 *
 * @retval zero on success.
 * @retval negative value on failure.
 */
void ether_selftest_run(struct net_device *dev,
			struct ethtool_test *etest, u64 *buf)
{
	struct ether_priv_data *pdata = netdev_priv(dev);
	struct osi_ioctl ioctl_data = {};
	int count = ether_selftest_get_count(pdata);
	int carrier = netif_carrier_ok(dev);
	int i, ret;

	if (!netif_running(dev)) {
		netdev_err(dev, "%s(): Interface is not up\n", __func__);
		return;
	}

	memset(buf, 0, sizeof(*buf) * count);

	netif_carrier_off(dev);

	for (i = 0; i < count; i++) {
		ret = 0;

		switch (ether_selftests[i].lb) {
		case ETHER_LOOPBACK_PHY:
			ret = -EOPNOTSUPP;
			if (dev->phydev)
				ret = phy_loopback(dev->phydev, true);
			if (!ret)
				break;
		/* Fallthrough */
			fallthrough;
		case ETHER_LOOPBACK_MAC:
			if (pdata->osi_core) {
				ioctl_data.cmd = OSI_CMD_MAC_LB;
				ioctl_data.arg1_u32 = OSI_ENABLE;
				ret = osi_handle_ioctl(pdata->osi_core,
						       &ioctl_data);
			}
			break;
		default:
			ret = -EOPNOTSUPP;
			break;
		}

		if (ret) {
			netdev_err(dev, "Loopback is not supported\n");
			etest->flags |= ETH_TEST_FL_FAILED;
			break;
		}

		ret = ether_selftests[i].fn(pdata);
		if (ret && (ret != -EOPNOTSUPP))
			etest->flags |= ETH_TEST_FL_FAILED;
		buf[i] = ret;

		switch (ether_selftests[i].lb) {
		case ETHER_LOOPBACK_PHY:
			ret = -EOPNOTSUPP;
			if (dev->phydev)
				ret = phy_loopback(dev->phydev, false);
			if (!ret)
				break;
		/* Fallthrough */
			fallthrough;
		case ETHER_LOOPBACK_MAC:
			if (pdata->osi_core) {
				ioctl_data.cmd = OSI_CMD_MAC_LB;
				ioctl_data.arg1_u32 = OSI_DISABLE;
				ret = osi_handle_ioctl(pdata->osi_core,
						       &ioctl_data);
			}
			if (!ret)
				break;
		default:
			break;
		}
	}

	/* Restart everything */
	if (carrier)
		netif_carrier_on(dev);
}

/**
 * @brief ether_selftest_get_strings - Getting strings for selftests
 *
 * @param[in] pdata: Ethernet OSD private data
 * @param[in] data: Data pointer to store strings for selftest names
 */
void ether_selftest_get_strings(struct ether_priv_data *pdata, u8 *data)
{
	u8 *p = data;
	int i;

	for (i = 0; i < ether_selftest_get_count(pdata); i++) {
		scnprintf(p, ETH_GSTRING_LEN, "%2d. %s", i + 1,
			  ether_selftests[i].name);
		p += ETH_GSTRING_LEN;
	}
}

/**
 * @brief ether_selftest_get_count - Count for Ethernet selftests
 *
 * @param[in] pdata: Ethernet OSD private data
 *
 * @retval Number of Ethernet selftests
 */
int ether_selftest_get_count(struct ether_priv_data *pdata)
{
	return ARRAY_SIZE(ether_selftests);
}
#endif /* OSI_STRIPPED_LIB */
