Contributors: 2
Author Tokens Token Proportion Commits Commit Proportion
Heikki Krogerus 1085 99.45% 2 66.67%
Dan Carpenter 6 0.55% 1 33.33%
Total 1091 3


// SPDX-License-Identifier: GPL-2.0
/*
 * USB Type-C Connector Class Port Mapping Utility
 *
 * Copyright (C) 2021, Intel Corporation
 * Author: Heikki Krogerus <heikki.krogerus@linux.intel.com>
 */

#include <linux/acpi.h>
#include <linux/usb.h>
#include <linux/usb/typec.h>

#include "class.h"

struct port_node {
	struct list_head list;
	struct device *dev;
	void *pld;
};

static int acpi_pld_match(const struct acpi_pld_info *pld1,
			  const struct acpi_pld_info *pld2)
{
	if (!pld1 || !pld2)
		return 0;

	/*
	 * To speed things up, first checking only the group_position. It seems
	 * to often have the first unique value in the _PLD.
	 */
	if (pld1->group_position == pld2->group_position)
		return !memcmp(pld1, pld2, sizeof(struct acpi_pld_info));

	return 0;
}

static void *get_pld(struct device *dev)
{
#ifdef CONFIG_ACPI
	struct acpi_pld_info *pld;
	acpi_status status;

	if (!has_acpi_companion(dev))
		return NULL;

	status = acpi_get_physical_device_location(ACPI_HANDLE(dev), &pld);
	if (ACPI_FAILURE(status))
		return NULL;

	return pld;
#else
	return NULL;
#endif
}

static void free_pld(void *pld)
{
#ifdef CONFIG_ACPI
	ACPI_FREE(pld);
#endif
}

static int __link_port(struct typec_port *con, struct port_node *node)
{
	int ret;

	ret = sysfs_create_link(&node->dev->kobj, &con->dev.kobj, "connector");
	if (ret)
		return ret;

	ret = sysfs_create_link(&con->dev.kobj, &node->dev->kobj,
				dev_name(node->dev));
	if (ret) {
		sysfs_remove_link(&node->dev->kobj, "connector");
		return ret;
	}

	list_add_tail(&node->list, &con->port_list);

	return 0;
}

static int link_port(struct typec_port *con, struct port_node *node)
{
	int ret;

	mutex_lock(&con->port_list_lock);
	ret = __link_port(con, node);
	mutex_unlock(&con->port_list_lock);

	return ret;
}

static void __unlink_port(struct typec_port *con, struct port_node *node)
{
	sysfs_remove_link(&con->dev.kobj, dev_name(node->dev));
	sysfs_remove_link(&node->dev->kobj, "connector");
	list_del(&node->list);
}

static void unlink_port(struct typec_port *con, struct port_node *node)
{
	mutex_lock(&con->port_list_lock);
	__unlink_port(con, node);
	mutex_unlock(&con->port_list_lock);
}

static struct port_node *create_port_node(struct device *port)
{
	struct port_node *node;

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

	node->dev = get_device(port);
	node->pld = get_pld(port);

	return node;
}

static void remove_port_node(struct port_node *node)
{
	put_device(node->dev);
	free_pld(node->pld);
	kfree(node);
}

static int connector_match(struct device *dev, const void *data)
{
	const struct port_node *node = data;

	if (!is_typec_port(dev))
		return 0;

	return acpi_pld_match(to_typec_port(dev)->pld, node->pld);
}

static struct device *find_connector(struct port_node *node)
{
	if (!node->pld)
		return NULL;

	return class_find_device(&typec_class, NULL, node, connector_match);
}

/**
 * typec_link_port - Link a port to its connector
 * @port: The port device
 *
 * Find the connector of @port and create symlink named "connector" for it.
 * Returns 0 on success, or errno in case of a failure.
 *
 * NOTE. The function increments the reference count of @port on success.
 */
int typec_link_port(struct device *port)
{
	struct device *connector;
	struct port_node *node;
	int ret;

	node = create_port_node(port);
	if (IS_ERR(node))
		return PTR_ERR(node);

	connector = find_connector(node);
	if (!connector) {
		ret = 0;
		goto remove_node;
	}

	ret = link_port(to_typec_port(connector), node);
	if (ret)
		goto put_connector;

	return 0;

put_connector:
	put_device(connector);
remove_node:
	remove_port_node(node);

	return ret;
}
EXPORT_SYMBOL_GPL(typec_link_port);

static int port_match_and_unlink(struct device *connector, void *port)
{
	struct port_node *node;
	struct port_node *tmp;
	int ret = 0;

	if (!is_typec_port(connector))
		return 0;

	mutex_lock(&to_typec_port(connector)->port_list_lock);
	list_for_each_entry_safe(node, tmp, &to_typec_port(connector)->port_list, list) {
		ret = node->dev == port;
		if (ret) {
			unlink_port(to_typec_port(connector), node);
			remove_port_node(node);
			put_device(connector);
			break;
		}
	}
	mutex_unlock(&to_typec_port(connector)->port_list_lock);

	return ret;
}

/**
 * typec_unlink_port - Unlink port from its connector
 * @port: The port device
 *
 * Removes the symlink "connector" and decrements the reference count of @port.
 */
void typec_unlink_port(struct device *port)
{
	class_for_each_device(&typec_class, NULL, port, port_match_and_unlink);
}
EXPORT_SYMBOL_GPL(typec_unlink_port);

static int each_port(struct device *port, void *connector)
{
	struct port_node *node;
	int ret;

	node = create_port_node(port);
	if (IS_ERR(node))
		return PTR_ERR(node);

	if (!connector_match(connector, node)) {
		remove_port_node(node);
		return 0;
	}

	ret = link_port(to_typec_port(connector), node);
	if (ret) {
		remove_port_node(node->pld);
		return ret;
	}

	get_device(connector);

	return 0;
}

int typec_link_ports(struct typec_port *con)
{
	int ret = 0;

	con->pld = get_pld(&con->dev);
	if (!con->pld)
		return 0;

	ret = usb_for_each_port(&con->dev, each_port);
	if (ret)
		typec_unlink_ports(con);

	return ret;
}

void typec_unlink_ports(struct typec_port *con)
{
	struct port_node *node;
	struct port_node *tmp;

	mutex_lock(&con->port_list_lock);

	list_for_each_entry_safe(node, tmp, &con->port_list, list) {
		__unlink_port(con, node);
		remove_port_node(node);
		put_device(&con->dev);
	}

	mutex_unlock(&con->port_list_lock);

	free_pld(con->pld);
}