// SPDX-License-Identifier: GPL-2.0 /* * USB Type-C Connector Class Port Mapping Utility * * Copyright (C) 2021, Intel Corporation * Author: Heikki Krogerus */ #include #include #include #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); }