/*
    V4L2 API subdev ioctl tests.

    Copyright (C) 2018  Hans Verkuil <hverkuil-cisco@xs4all.nl>

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA  02110-1335  USA
 */

#include <map>
#include <set>

#include <dirent.h>
#include <sys/sysmacros.h>
#include <sys/types.h>

#include "v4l2-compliance.h"

int testMediaDeviceInfo(struct node *node)
{
	struct media_device_info mdinfo;

	memset(&mdinfo, 0xff, sizeof(mdinfo));
	fail_on_test(doioctl(node, MEDIA_IOC_DEVICE_INFO, &mdinfo));
	if (has_mmu)
		fail_on_test(doioctl(node, MEDIA_IOC_DEVICE_INFO, nullptr) != EFAULT);
	fail_on_test(check_0(mdinfo.reserved, sizeof(mdinfo.reserved)));
	fail_on_test(check_string(mdinfo.driver, sizeof(mdinfo.driver)));
	fail_on_test(mdinfo.model[0] && check_string(mdinfo.model, sizeof(mdinfo.model)));
	fail_on_test(mdinfo.serial[0] && check_string(mdinfo.serial, sizeof(mdinfo.serial)));
	if (!mdinfo.bus_info[0]) {
		warn("empty bus_info\n");
	} else {
		fail_on_test(check_string(mdinfo.bus_info, sizeof(mdinfo.bus_info)));
		// Check for valid prefixes
		if (memcmp(mdinfo.bus_info, "usb-", 4) &&
		    memcmp(mdinfo.bus_info, "PCI:", 4) &&
		    memcmp(mdinfo.bus_info, "PCIe:", 5) &&
		    memcmp(mdinfo.bus_info, "ISA:", 4) &&
		    memcmp(mdinfo.bus_info, "I2C:", 4) &&
		    memcmp(mdinfo.bus_info, "parport", 7) &&
		    memcmp(mdinfo.bus_info, "platform:", 9) &&
		    memcmp(mdinfo.bus_info, "rmi4:", 5))
			return fail("missing bus_info prefix ('%s')\n", mdinfo.bus_info);
	}
	fail_on_test(mdinfo.media_version == 0);
	if (mdinfo.media_version != MEDIA_API_VERSION)
		fail_on_test(mdinfo.driver_version != mdinfo.media_version);
	node->media_version = mdinfo.media_version;
	return 0;
}

static int checkDevice(__u32 major, __u32 minor, bool iface, __u32 id)
{
	char dev_path[100];
	fail_on_test(snprintf(dev_path, sizeof(dev_path), "/sys/dev/char/%d:%d",
			      major, minor) == -1);
	DIR *dp = opendir(dev_path);
	if (dp == nullptr)
		return fail("couldn't find %s for %s %u\n",
			    dev_path, iface ? "interface" : "entity", id);
	closedir(dp);
	return 0;
}

using id_set = std::set<__u32>;

static media_v2_topology topology;
static media_v2_entity *v2_ents;
static id_set v2_entities_set;
static media_v2_interface *v2_ifaces;
static id_set v2_interfaces_set;
static media_v2_pad *v2_pads;
static id_set v2_pads_set;
static media_v2_link *v2_links;
static id_set v2_links_set;
static std::map<__u32, __u32> entity_num_pads;
static std::map<__u32, media_v2_entity *> v2_entity_map;
static std::set<std::string> v2_entity_names_set;
static std::map<__u32, media_v2_interface *> v2_iface_map;
static std::map<__u32, media_v2_pad *> v2_pad_map;
static std::set<__u64> v2_entity_pad_idx_set;
static unsigned num_data_links;

static int checkFunction(__u32 function, bool v2_api)
{
	fail_on_test(function == MEDIA_ENT_F_UNKNOWN);
	fail_on_test((function & MEDIA_ENT_TYPE_MASK) == MEDIA_ENT_T_DEVNODE &&
		     function != MEDIA_ENT_T_DEVNODE_V4L &&
		     function != MEDIA_ENT_T_DEVNODE_UNKNOWN);
	fail_on_test((function & MEDIA_ENT_TYPE_MASK) == MEDIA_ENT_F_OLD_SUBDEV_BASE &&
		     function > MEDIA_ENT_F_TUNER);
	if (!v2_api)
		return 0;
	// The old API can return these due to a horrible workaround in
	// media-device.c. But for the v2 API these should never be returned.
	fail_on_test(function == MEDIA_ENT_F_V4L2_SUBDEV_UNKNOWN);
	fail_on_test(function == MEDIA_ENT_T_DEVNODE_UNKNOWN);
	return 0;
}

int testMediaTopology(struct node *node)
{
	memset(&topology, 0xff, sizeof(topology));
	topology.ptr_entities = 0;
	topology.ptr_interfaces = 0;
	topology.ptr_pads = 0;
	topology.ptr_links = 0;
	fail_on_test(doioctl(node, MEDIA_IOC_G_TOPOLOGY, &topology));
	fail_on_test(!topology.num_entities);
	fail_on_test(topology.topology_version == ~0ULL);
	fail_on_test(topology.num_entities == ~0U);
	fail_on_test(topology.num_interfaces == ~0U);
	fail_on_test(topology.num_pads == ~0U);
	fail_on_test(topology.num_links == ~0U);
	fail_on_test(topology.reserved1);
	fail_on_test(topology.reserved2);
	fail_on_test(topology.reserved3);
	fail_on_test(topology.reserved4);
	if (has_mmu) {
		topology.ptr_entities = 4;
		fail_on_test(doioctl(node, MEDIA_IOC_G_TOPOLOGY, &topology) != EFAULT);
		topology.ptr_entities = 0;
		topology.ptr_interfaces = 4;
		fail_on_test(topology.num_interfaces &&
			     doioctl(node, MEDIA_IOC_G_TOPOLOGY, &topology) != EFAULT);
		topology.ptr_interfaces = 0;
		topology.ptr_pads = 4;
		fail_on_test(topology.num_pads &&
			     doioctl(node, MEDIA_IOC_G_TOPOLOGY, &topology) != EFAULT);
		topology.ptr_pads = 0;
		topology.ptr_links = 4;
		fail_on_test(topology.num_links &&
			     doioctl(node, MEDIA_IOC_G_TOPOLOGY, &topology) != EFAULT);
		topology.ptr_links = 0;
	}
	v2_ents = new media_v2_entity[topology.num_entities];
	memset(v2_ents, 0xff, topology.num_entities * sizeof(*v2_ents));
	topology.ptr_entities = (uintptr_t)v2_ents;
	v2_ifaces = new media_v2_interface[topology.num_interfaces];
	memset(v2_ifaces, 0xff, topology.num_interfaces * sizeof(*v2_ifaces));
	topology.ptr_interfaces = (uintptr_t)v2_ifaces;
	v2_pads = new media_v2_pad[topology.num_pads];
	memset(v2_pads, 0xff, topology.num_pads * sizeof(*v2_pads));
	topology.ptr_pads = (uintptr_t)v2_pads;
	v2_links = new media_v2_link[topology.num_links];
	memset(v2_links, 0xff, topology.num_links * sizeof(*v2_links));
	topology.ptr_links = (uintptr_t)v2_links;
	fail_on_test(doioctl(node, MEDIA_IOC_G_TOPOLOGY, &topology));
	fail_on_test(v2_ents != (media_v2_entity *)topology.ptr_entities);
	fail_on_test(v2_ifaces != (media_v2_interface *)topology.ptr_interfaces);
	fail_on_test(v2_pads != (media_v2_pad *)topology.ptr_pads);
	fail_on_test(v2_links != (media_v2_link *)topology.ptr_links);

	for (unsigned i = 0; i < topology.num_entities; i++) {
		media_v2_entity &ent = v2_ents[i];
		std::string key = ent.name;

		/*
		 * The v2_entity_names_set set is used to check if a specific
		 * entity name is reused, and to check if the topology matches
		 * what ENUM_ENTITIES returns.
		 *
		 * However, the size of the entity name array of media_v2_entity
		 * is 64 characters, while it is 32 for media_entity_desc.
		 *
		 * So cut off the last 32 characters before storing the key.
		 * This means that the first 31 characters of the entity name
		 * must be unique, otherwise ENUM_ENTITIES would return
		 * duplicate entity names.
		 */
		if (key.length() >= 32)
			key.erase(31, key.length() - 1);

		if (show_info) {
			printf("\t\tEntity: 0x%08x (Name: '%s', Function: %s",
			       ent.id, ent.name, mi_entfunction2s(ent.function).c_str());
			if (MEDIA_V2_ENTITY_HAS_FLAGS(node->media_version) && ent.flags)
				printf(", Flags: %s", mi_entflags2s(ent.flags).c_str());
			printf(")\n");
		}
		fail_on_test(check_0(ent.reserved, sizeof(ent.reserved)));
		fail_on_test(check_string(ent.name, sizeof(ent.name)));
		fail_on_test(!ent.id);
		fail_on_test(checkFunction(ent.function, true));
		fail_on_test(v2_entities_set.find(ent.id) != v2_entities_set.end());
		fail_on_test(v2_entity_names_set.find(key) != v2_entity_names_set.end());
		if (!MEDIA_V2_ENTITY_HAS_FLAGS(node->media_version))
			fail_on_test(ent.flags);
		v2_entities_set.insert(ent.id);
		v2_entity_names_set.insert(key);
		v2_entity_map[ent.id] = &ent;
	}
	for (unsigned i = 0; i < topology.num_interfaces; i++) {
		media_v2_interface &iface = v2_ifaces[i];
		dev_t dev = makedev(iface.devnode.major, iface.devnode.minor);
		std::string devpath = mi_get_devpath_from_dev_t(dev);

		if (show_info)
			printf("\t\tInterface: 0x%08x (Type: %s, DevPath: %s)\n",
			       iface.id, mi_ifacetype2s(iface.intf_type).c_str(),
			       devpath.c_str());
		fail_on_test(devpath.empty());
		fail_on_test(check_0(iface.reserved, sizeof(iface.reserved)));
		fail_on_test(checkDevice(iface.devnode.major, iface.devnode.minor,
					 true, iface.id));
		fail_on_test(!iface.id);
		fail_on_test(!iface.intf_type);
		fail_on_test(iface.intf_type < MEDIA_INTF_T_DVB_BASE);
		fail_on_test(iface.intf_type > MEDIA_INTF_T_ALSA_BASE + 0xff);
		fail_on_test(iface.flags);
		fail_on_test(v2_interfaces_set.find(iface.id) != v2_interfaces_set.end());
		v2_interfaces_set.insert(iface.id);
		v2_iface_map[iface.id] = &iface;
	}
	for (unsigned i = 0; i < topology.num_pads; i++) {
		media_v2_pad &pad = v2_pads[i];
		__u32 fl = pad.flags;

		if (show_info) {
			printf("\t\tPad: 0x%08x (", pad.id);
			if (MEDIA_V2_PAD_HAS_INDEX(node->media_version))
				printf("%u, ", pad.index);
			printf("%s, %s)\n",
			       v2_entity_map[pad.entity_id]->name,
			       mi_padflags2s(pad.flags).c_str());
		}
		fail_on_test(check_0(pad.reserved, sizeof(pad.reserved)));
		fail_on_test(!pad.id);
		fail_on_test(!pad.entity_id);
		fail_on_test(v2_pads_set.find(pad.id) != v2_pads_set.end());
		v2_pads_set.insert(pad.id);
		fail_on_test(v2_entities_set.find(pad.entity_id) == v2_entities_set.end());
		fail_on_test(!(fl & (MEDIA_PAD_FL_SINK | MEDIA_PAD_FL_SOURCE)));
		fail_on_test((fl & (MEDIA_PAD_FL_SINK | MEDIA_PAD_FL_SOURCE)) ==
			     (MEDIA_PAD_FL_SINK | MEDIA_PAD_FL_SOURCE));
		if (MEDIA_V2_PAD_HAS_INDEX(node->media_version)) {
			fail_on_test(pad.index == ~0U);
			fail_on_test(v2_entity_pad_idx_set.find((__u64)pad.entity_id << 32 | pad.index) !=
				     v2_entity_pad_idx_set.end());
			v2_entity_pad_idx_set.insert(static_cast<__u64>(pad.entity_id) << 32 | pad.index);
		} else {
			fail_on_test(pad.index);
		}
		entity_num_pads[pad.entity_id]++;
		v2_pad_map[pad.id] = &pad;
	}

	std::set<__u32> ents_with_intf;

	for (unsigned i = 0; i < topology.num_links; i++) {
		media_v2_link &link = v2_links[i];
		bool is_iface = (link.flags & MEDIA_LNK_FL_LINK_TYPE) ==
			MEDIA_LNK_FL_INTERFACE_LINK;
		bool is_anc_link = (link.flags & MEDIA_LNK_FL_LINK_TYPE) ==
			MEDIA_LNK_FL_ANCILLARY_LINK;

		fail_on_test(check_0(link.reserved, sizeof(link.reserved)));
		fail_on_test(!link.id);
		fail_on_test(!link.source_id);
		fail_on_test(!link.sink_id);
		fail_on_test(v2_links_set.find(link.id) != v2_links_set.end());
		v2_links_set.insert(link.id);
		if (is_iface) {
			fail_on_test(v2_interfaces_set.find(link.source_id) == v2_interfaces_set.end());
			fail_on_test(v2_entities_set.find(link.sink_id) == v2_entities_set.end());

			media_v2_interface &iface = *v2_iface_map[link.source_id];
			dev_t dev = makedev(iface.devnode.major, iface.devnode.minor);
			std::string devpath = mi_get_devpath_from_dev_t(dev);
			media_v2_entity &ent = *v2_entity_map[link.sink_id];

			if (show_info)
				printf("\t\tInterface Link: 0x%08x (%s to %s)\n", link.id,
				       ent.name, devpath.c_str());
			ents_with_intf.insert(ent.id);
		} else if (is_anc_link) {
			fail_on_test(v2_entities_set.find(link.source_id) == v2_entities_set.end());
			fail_on_test(v2_entities_set.find(link.sink_id) == v2_entities_set.end());
			media_v2_entity &src_ent = *v2_entity_map[link.source_id];
			media_v2_entity &sink_ent = *v2_entity_map[link.sink_id];
			if (show_info)
				printf("\t\tAncillary Link: 0x%08x (%s <-> %s)\n", link.id,
				       src_ent.name, sink_ent.name);
		} else {
			fail_on_test(v2_pads_set.find(link.source_id) == v2_pads_set.end());
			fail_on_test(v2_pads_set.find(link.sink_id) == v2_pads_set.end());
			fail_on_test(link.source_id == link.sink_id);
			fail_on_test(!(v2_pad_map[link.source_id]->flags & MEDIA_PAD_FL_SOURCE));
			fail_on_test(!(v2_pad_map[link.sink_id]->flags & MEDIA_PAD_FL_SINK));
			num_data_links++;
			if (show_info)
				printf("\t\tData Link: 0x%08x (%s:%u -> %s:%u, %s)\n", link.id,
				       v2_entity_map[v2_pad_map[link.source_id]->entity_id]->name,
				       v2_pad_map[link.source_id]->index,
				       v2_entity_map[v2_pad_map[link.sink_id]->entity_id]->name,
				       v2_pad_map[link.sink_id]->index,
				       mi_linkflags2s(link.flags).c_str());
		}
	}

	for (unsigned i = 0; i < topology.num_entities; i++) {
		media_v2_entity &ent = v2_ents[i];

		fail_on_test(mi_func_requires_intf(ent.function) &&
			     ents_with_intf.find(ent.id) == ents_with_intf.end());
	}
	node->topology = &topology;
	return 0;
}

static media_link_desc link_immutable;
static media_link_desc link_enabled;
static media_link_desc link_disabled;

int testMediaEnum(struct node *node)
{
	using entity_map = std::map<__u32, media_entity_desc>;
	entity_map ent_map;
	id_set has_default_set;
	struct media_entity_desc ent;
	struct media_links_enum links;
	unsigned num_links = 0;
	__u32 last_id = 0;
	int ret;

	memset(&ent, 0, sizeof(ent));
	// Entity ID 0 can never occur
	ent.id = 0;
	fail_on_test(doioctl(node, MEDIA_IOC_ENUM_ENTITIES, &ent) != EINVAL);
	for (;;) {
		memset(&ent, 0xff, sizeof(ent));
		ent.id = last_id | MEDIA_ENT_ID_FLAG_NEXT;
		ret = doioctl(node, MEDIA_IOC_ENUM_ENTITIES, &ent);
		if (ret == EINVAL)
			break;
		dev_t dev = makedev(ent.dev.major, ent.dev.minor);
		std::string devpath = mi_get_devpath_from_dev_t(dev);

		if (show_info) {
			printf("\t\tEntity: 0x%08x (Name: '%s', Type: %s",
			       ent.id, ent.name, mi_entfunction2s(ent.type).c_str());
			if (ent.flags)
				printf(", Flags: %s", mi_entflags2s(ent.flags).c_str());
			if (!devpath.empty())
				printf(", DevPath: %s", devpath.c_str());
			printf(")\n");
		}
		fail_on_test(dev && devpath.empty());
		fail_on_test(check_0(ent.reserved, sizeof(ent.reserved)));
		fail_on_test(ent.id & MEDIA_ENT_ID_FLAG_NEXT);
		fail_on_test(!ent.id);
		fail_on_test(ent.id <= last_id);
		last_id = ent.id;
		fail_on_test(check_string(ent.name, sizeof(ent.name)));
		fail_on_test(ent.revision);
		fail_on_test(ent.group_id);
		fail_on_test(ent.type == ~0U);
		fail_on_test(checkFunction(ent.type, false));
		fail_on_test(ent.flags == ~0U);
		fail_on_test(ent.pads == 0xffff);
		fail_on_test(ent.links == 0xffff);

		if (ent.flags & MEDIA_ENT_FL_DEFAULT) {
			fail_on_test(has_default_set.find(ent.type) != has_default_set.end());
			has_default_set.insert(ent.type);
		}
		if (!(ent.flags & MEDIA_ENT_FL_CONNECTOR)) {
			fail_on_test(ent.dev.major == ~0U);
			fail_on_test(ent.dev.minor == ~0U);
			if (ent.dev.major || ent.dev.minor)
				fail_on_test(checkDevice(ent.dev.major, ent.dev.minor,
							 false, ent.id));
		}
		fail_on_test(doioctl(node, MEDIA_IOC_ENUM_ENTITIES, &ent));
		num_links += ent.links;

		if (node->topology) {
			fail_on_test(v2_entities_set.find(ent.id) == v2_entities_set.end());
			fail_on_test(v2_entity_names_set.find(ent.name) == v2_entity_names_set.end());
			fail_on_test(entity_num_pads[ent.id] != ent.pads);
			// Commented out due to horrible workaround in media-device.c
			//fail_on_test(v2_entity_map[ent.id]->function != ent.type);
		}
		ent_map[ent.id] = ent;
	}
	fail_on_test(num_data_links != num_links);

	for (auto & iter : ent_map) {
		media_entity_desc &ent = iter.second;

		memset(&links, 0, sizeof(links));
		memset(&links.reserved, 0xff, sizeof(links.reserved));
		links.entity = ent.id;
		fail_on_test(doioctl(node, MEDIA_IOC_ENUM_LINKS, &links));
		fail_on_test(check_0(links.reserved, sizeof(links.reserved)));
		fail_on_test(links.entity != ent.id);
		fail_on_test(links.pads);
		fail_on_test(links.links);
		if (has_mmu) {
			links.pads = (struct media_pad_desc *)4;
			fail_on_test(ent.pads && doioctl(node, MEDIA_IOC_ENUM_LINKS, &links) != EFAULT);
			links.pads = nullptr;
			links.links = (struct media_link_desc *)4;
			fail_on_test(ent.links && doioctl(node, MEDIA_IOC_ENUM_LINKS, &links) != EFAULT);
			links.links = nullptr;
		}
		links.pads = new media_pad_desc[ent.pads];
		memset(links.pads, 0xff, ent.pads * sizeof(*links.pads));
		links.links = new media_link_desc[ent.links];
		memset(links.links, 0xff, ent.links * sizeof(*links.links));
		memset(&links.reserved, 0xff, sizeof(links.reserved));
		fail_on_test(doioctl(node, MEDIA_IOC_ENUM_LINKS, &links));
		fail_on_test(check_0(links.reserved, sizeof(links.reserved)));

		for (unsigned i = 0; i < ent.pads; i++) {
			fail_on_test(links.pads[i].entity != ent.id);
			fail_on_test(links.pads[i].index == 0xffff);
			fail_on_test(check_0(links.pads[i].reserved, sizeof(links.pads[i].reserved)));
			__u32 fl = links.pads[i].flags;
			fail_on_test(!(fl & (MEDIA_PAD_FL_SINK | MEDIA_PAD_FL_SOURCE)));
			fail_on_test((fl & (MEDIA_PAD_FL_SINK | MEDIA_PAD_FL_SOURCE)) ==
				     (MEDIA_PAD_FL_SINK | MEDIA_PAD_FL_SOURCE));
			if (node->topology &&
			    MEDIA_V2_PAD_HAS_INDEX(node->media_version)) {
				__u64 key = static_cast<__u64>(ent.id) << 32 | links.pads[i].index;

				fail_on_test(v2_entity_pad_idx_set.find(key) ==
					     v2_entity_pad_idx_set.end());
				v2_entity_pad_idx_set.erase(key);
			}
		}
		bool found_enabled = false;
		for (unsigned i = 0; i < ent.links; i++) {
			bool is_sink = links.links[i].sink.entity == ent.id;
			__u32 fl = links.links[i].flags;
			__u32 remote_ent;
			__u16 remote_pad;

			fail_on_test(links.links[i].source.entity != ent.id &&
				     links.links[i].sink.entity != ent.id);
			fail_on_test(check_0(links.links[i].reserved, sizeof(links.links[i].reserved)));
			if (fl & MEDIA_LNK_FL_IMMUTABLE) {
				fail_on_test(!(fl & MEDIA_LNK_FL_ENABLED));
				fail_on_test(fl & MEDIA_LNK_FL_DYNAMIC);
				if (!link_immutable.source.entity)
					link_immutable = links.links[i];
			}
			if (fl & MEDIA_LNK_FL_DYNAMIC)
				fail_on_test(fl & MEDIA_LNK_FL_IMMUTABLE);
			if (is_sink && (fl & MEDIA_LNK_FL_ENABLED)) {
				// only one incoming link can be enabled
				fail_on_test(found_enabled);
				found_enabled = true;
			}
			if ((fl & MEDIA_LNK_FL_ENABLED) && !link_enabled.source.entity)
				link_enabled = links.links[i];
			if (!(fl & (MEDIA_LNK_FL_IMMUTABLE | MEDIA_LNK_FL_ENABLED)) &&
			    !link_disabled.source.entity)
				link_disabled = links.links[i];

			// This ioctl only returns data links
			fail_on_test((fl & MEDIA_LNK_FL_LINK_TYPE) != MEDIA_LNK_FL_DATA_LINK);
			fail_on_test(links.links[i].sink.entity == links.links[i].source.entity);
			if (is_sink) {
				fail_on_test(links.links[i].sink.index >= ent.pads);
				remote_ent = links.links[i].source.entity;
				remote_pad = links.links[i].source.index;
			} else {
				fail_on_test(links.links[i].source.index >= ent.pads);
				remote_ent = links.links[i].sink.entity;
				remote_pad = links.links[i].sink.index;
			}
			fail_on_test(ent_map.find(remote_ent) == ent_map.end());
			media_entity_desc &remote = ent_map[remote_ent];
			fail_on_test(remote_pad >= remote.pads);
		}
	}

	memset(&links, 0, sizeof(links));
	fail_on_test(doioctl(node, MEDIA_IOC_ENUM_LINKS, &links) != EINVAL);

	for (unsigned i = 0; i < topology.num_entities; i++)
		fail_on_test(ent_map.find(v2_ents[i].id) == ent_map.end());

	if (node->topology &&
	    MEDIA_V2_PAD_HAS_INDEX(node->media_version))
		fail_on_test(!v2_entity_pad_idx_set.empty());
	return 0;
}

int testMediaSetupLink(struct node *node)
{
	struct media_link_desc link;
	int ret;

	memset(&link, 0, sizeof(link));
	ret = doioctl(node, MEDIA_IOC_SETUP_LINK, &link);
	if (ret == ENOTTY)
		return ret;
	fail_on_test(ret != EINVAL);
	if (link_immutable.source.entity) {
		link = link_immutable;
		fail_on_test(doioctl(node, MEDIA_IOC_SETUP_LINK, &link));
		link.flags = MEDIA_LNK_FL_ENABLED;
		fail_on_test(doioctl(node, MEDIA_IOC_SETUP_LINK, &link) != EINVAL);
	}
	if (link_disabled.source.entity) {
		link = link_disabled;
		memset(link.reserved, 0xff, sizeof(link.reserved));
		fail_on_test(doioctl(node, MEDIA_IOC_SETUP_LINK, &link));
		fail_on_test(check_0(link.reserved, sizeof(link.reserved)));
		link.flags |= MEDIA_LNK_FL_INTERFACE_LINK;
		fail_on_test(doioctl(node, MEDIA_IOC_SETUP_LINK, &link) != EINVAL);
	}
	if (link_enabled.source.entity) {
		link = link_enabled;
		memset(link.reserved, 0xff, sizeof(link.reserved));
		fail_on_test(doioctl(node, MEDIA_IOC_SETUP_LINK, &link));
		fail_on_test(check_0(link.reserved, sizeof(link.reserved)));
		link.flags |= MEDIA_LNK_FL_INTERFACE_LINK;
		fail_on_test(doioctl(node, MEDIA_IOC_SETUP_LINK, &link) != EINVAL);
	}
	return 0;
}

void walkTopology(struct node &node, struct node &expbuf_node,
		  unsigned frame_count, unsigned all_fmt_frame_count)
{
	media_v2_topology topology;

	memset(&topology, 0, sizeof(topology));
	if (ioctl(node.g_fd(), MEDIA_IOC_G_TOPOLOGY, &topology))
		return;

	media_v2_interface v2_ifaces[topology.num_interfaces];

	topology.ptr_interfaces = (uintptr_t)v2_ifaces;
	if (ioctl(node.g_fd(), MEDIA_IOC_G_TOPOLOGY, &topology))
		return;

	for (unsigned i = 0; i < topology.num_interfaces; i++) {
		media_v2_interface &iface = v2_ifaces[i];
		std::string dev = mi_media_get_device(iface.devnode.major,
						      iface.devnode.minor);
		if (dev.empty())
			continue;

		printf("--------------------------------------------------------------------------------\n");

		media_type type = mi_media_detect_type(dev.c_str());
		if (type == MEDIA_TYPE_CANT_STAT) {
			fprintf(stderr, "\nCannot open device %s, skipping.\n\n",
				dev.c_str());
			continue;
		}

		switch (type) {
		// For now we can only handle V4L2 devices
		case MEDIA_TYPE_VIDEO:
		case MEDIA_TYPE_VBI:
		case MEDIA_TYPE_RADIO:
		case MEDIA_TYPE_SDR:
		case MEDIA_TYPE_TOUCH:
		case MEDIA_TYPE_SUBDEV:
			break;
		default:
			type = MEDIA_TYPE_UNKNOWN;
			break;
		}

		if (type == MEDIA_TYPE_UNKNOWN) {
			fprintf(stderr, "\nUnable to detect what device %s is, skipping.\n\n",
				dev.c_str());
			continue;
		}

		struct node test_node;
		int fd = -1;

		test_node.device = dev.c_str();
		test_node.s_trace(node.g_trace());
		switch (type) {
		case MEDIA_TYPE_MEDIA:
			test_node.s_direct(true);
			fd = test_node.media_open(dev.c_str(), false);
			break;
		case MEDIA_TYPE_SUBDEV:
			test_node.s_direct(true);
			fd = test_node.subdev_open(dev.c_str(), false);
			break;
		default:
			test_node.s_direct(node.g_direct());
			fd = test_node.open(dev.c_str(), false);
			break;
		}
		if (fd < 0) {
			fprintf(stderr, "\nFailed to open device %s, skipping\n\n",
				dev.c_str());
			continue;
		}

		testNode(test_node, test_node, expbuf_node, type,
			 frame_count, all_fmt_frame_count, node.g_fd());
		test_node.close();
	}
}
