/*
 * Copyright (C) 2013 Red Hat, Inc.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library.  If not, see
 * <http://www.gnu.org/licenses/>.
 */

#include <config.h>

#if defined(__linux__) || defined(__FreeBSD__)
# include "virmock.h"
# include <unistd.h>
# include <fcntl.h>
# include <sys/stat.h>
# include <stdarg.h>
# include <dirent.h>
# include "viralloc.h"
# include "virstring.h"
# include "virfile.h"

static int (*real_access)(const char *path, int mode);
static int (*real_open)(const char *path, int flags, ...);
# ifdef __GLIBC__
static int (*real___open_2)(const char *path, int flags);
# endif /* ! __GLIBC__ */
static int (*real_close)(int fd);
static DIR * (*real_opendir)(const char *name);
static char *(*real_virFileCanonicalizePath)(const char *path);

static char *fakerootdir;

/* To add a new mocked prefix in virpcimock:
 * - add the prefix here as a define to make it easier to track what we
 * are mocking;
 * - add it to the 'pathPrefixIsMocked()' helper;
 * - (optional) edit 'getrealpath()' if you need the resulting mocked
 * path to be different than <fakerootdir>/path
 */
# define SYSFS_PCI_PREFIX "/sys/bus/pci/"
# define SYSFS_KERNEL_PREFIX "/sys/kernel/"
# define DEV_VFIO_PREFIX "/dev/vfio/"

# define STDERR(...) \
    fprintf(stderr, "%s %zu: ", __FUNCTION__, (size_t) __LINE__); \
    fprintf(stderr, __VA_ARGS__); \
    fprintf(stderr, "\n"); \

# define ABORT(...) \
    do { \
        STDERR(__VA_ARGS__); \
        abort(); \
    } while (0)

# define ABORT_OOM() \
    ABORT("Out of memory")
/*
 * The plan:
 *
 * Mock some file handling functions. Redirect them into a stub tree passed via
 * LIBVIRT_FAKE_ROOT_DIR env variable. All files and links within stub tree is
 * created by us. There are some actions that we must take if some special
 * files are written to. Here's the list of files we watch:
 *
 * /sys/bus/pci/drivers/<driver>/bind
 *   Check if driver supports the device and bind driver to it (create symlink
 *   called 'driver' pointing to the /sys/but/pci/drivers/<driver>).
 *   Data in format "DDDD:BB:DD.F" (Domain:Bus:Device.Function).
 *
 * /sys/bus/pci/drivers/<driver>/unbind
 *   Unbind driver from the device.
 *   Data in format "DDDD:BB:DD.F" (Domain:Bus:Device.Function).
 *
 * /sys/bus/pci/drivers_probe
 *   Probe for a driver that handles the specified device.
 *   Data in format "DDDD:BB:DD.F" (Domain:Bus:Device.Function).
 *
 * /sys/bus/pci/devices/<device>/driver_override
 *   Name of a driver that overrides preferred driver can be written
 *   here. The device will be attached to it on drivers_probe event.
 *   Writing an empty string (or "\n") clears the override.
 *
 * As a little hack, we are not mocking write to these files, but close()
 * instead. The advantage is we don't need any self growing array to hold the
 * partial writes and construct them back. We can let all the writes finish,
 * and then just read the file content back.
 */

/*
 *
 * Functions to model kernel behavior
 *
 */

struct pciDriver {
    char *name;
    int *vendor;        /* List of vendor:device IDs the driver can handle */
    int *device;
    size_t len;            /* @len is used for both @vendor and @device */
};

struct pciIommuGroup {
    int iommu;
    size_t nDevicesBoundToVFIO; /* Indicates the devices in the group */
};

struct pciDeviceAddress {
    unsigned int domain;
    unsigned int bus;
    unsigned int device;
    unsigned int function;
};
# define ADDR_STR_FMT "%04x:%02x:%02x.%d"

struct pciDevice {
    struct pciDeviceAddress addr;
    int vendor;
    int device;
    int klass;
    int iommuGroup;
    const char *physfn;
    struct pciDriver *driver;   /* Driver attached. NULL if attached to no driver */
};

struct fdCallback {
    int fd;
    char *path;
};

struct pciDevice **pciDevices = NULL;
size_t nPCIDevices = 0;

struct pciDriver **pciDrivers = NULL;
size_t nPCIDrivers = 0;

struct pciIommuGroup **pciIommuGroups = NULL;
size_t npciIommuGroups = 0;

struct fdCallback *callbacks = NULL;
size_t nCallbacks = 0;

static void init_env(void);

static int pci_device_autobind(struct pciDevice *dev);
static void pci_device_new_from_stub(const struct pciDevice *data);
static struct pciDevice *pci_device_find_by_id(struct pciDeviceAddress const *addr);
static struct pciDevice *pci_device_find_by_content(const char *path);

static void pci_driver_new(const char *name, ...);
static struct pciDriver *pci_driver_find_by_dev(struct pciDevice *dev);
static struct pciDriver *pci_driver_find_by_path(const char *path);
static struct pciDriver *pci_driver_find_by_driver_override(struct pciDevice *dev);
static int pci_driver_bind(struct pciDriver *driver, struct pciDevice *dev);
static int pci_driver_unbind(struct pciDriver *driver, struct pciDevice *dev);
static int pci_driver_handle_change(int fd, const char *path);
static int pci_driver_handle_bind(const char *path);
static int pci_driver_handle_unbind(const char *path);


/*
 * Helper functions
 */
static void
make_file(const char *path,
          const char *name,
          const char *value,
          ssize_t len)
{
    int fd = -1;
    g_autofree char *filepath = NULL;
    if (value && len == -1)
        len = strlen(value);

    filepath = g_strdup_printf("%s/%s", path, name);

    if ((fd = real_open(filepath, O_CREAT|O_WRONLY, 0666)) < 0)
        ABORT("Unable to open: %s", filepath);

    if (value && safewrite(fd, value, len) != len)
        ABORT("Unable to write: %s", filepath);

    VIR_FORCE_CLOSE(fd);
}

static void
make_dir(const char *path,
         const char *name)
{
    g_autofree char *dirpath = NULL;

    dirpath = g_strdup_printf("%s/%s", path, name);

    if (virFileMakePath(dirpath) < 0)
        ABORT("Unable to create: %s", dirpath);
}

static void
make_symlink(const char *path,
          const char *name,
          const char *target)
{
    g_autofree char *filepath = NULL;

    filepath = g_strdup_printf("%s/%s", path, name);

    if (symlink(target, filepath) < 0)
        ABORT("Unable to create symlink filepath -> target");
}

static int
pci_read_file(const char *path,
              char *buf,
              size_t buf_size,
              bool truncate)
{
    int ret = -1;
    int fd = -1;
    g_autofree char *newpath = NULL;

    newpath = g_strdup_printf("%s/%s", fakerootdir, path);

    if ((fd = real_open(newpath, O_RDWR)) < 0)
        goto cleanup;

    bzero(buf, buf_size);
    if (saferead(fd, buf, buf_size - 1) < 0) {
        STDERR("Unable to read from %s", newpath);
        goto cleanup;
    }

    if (truncate &&
        ftruncate(fd, 0) < 0)
        goto cleanup;

    ret = 0;
 cleanup:
    real_close(fd);
    return ret;
}

static bool
pathPrefixIsMocked(const char *path)
{
    return STRPREFIX(path, SYSFS_PCI_PREFIX) ||
           STRPREFIX(path, SYSFS_KERNEL_PREFIX) ||
           STRPREFIX(path, DEV_VFIO_PREFIX);
}

static int
getrealpath(char **newpath,
            const char *path)
{
    if (!fakerootdir && pathPrefixIsMocked(path))
        init_env();

    if (STRPREFIX(path, SYSFS_PCI_PREFIX)) {
        *newpath = g_strdup_printf("%s/sys/bus/pci/%s",
                                   fakerootdir,
                                   path + strlen(SYSFS_PCI_PREFIX));
    } else if (pathPrefixIsMocked(path)) {
        *newpath = g_strdup_printf("%s/%s",
                                   fakerootdir,
                                   path);
    } else {
        *newpath = g_strdup(path);
    }

    return 0;
}

static bool
find_fd(int fd, size_t *indx)
{
    size_t i;

    for (i = 0; i < nCallbacks; i++) {
        if (callbacks[i].fd == fd) {
            if (indx)
                *indx = i;
            return true;
        }
    }

    return false;
}

static int
add_fd(int fd, const char *path)
{
    size_t i;

    if (find_fd(fd, &i)) {
        struct fdCallback cb = callbacks[i];
        ABORT("FD %d %s already present in the array as %d %s",
              fd, path, cb.fd, cb.path);
    }

    if (VIR_REALLOC_N_QUIET(callbacks, nCallbacks + 1) < 0) {
        errno = ENOMEM;
        return -1;
    }

    callbacks[nCallbacks].path = g_strdup(path);
    callbacks[nCallbacks++].fd = fd;

    return 0;
}

static int
remove_fd(int fd)
{
    size_t i;

    if (find_fd(fd, &i)) {
        struct fdCallback cb = callbacks[i];

        if (pci_driver_handle_change(cb.fd, cb.path) < 0)
            return -1;

        VIR_FREE(cb.path);
        if (VIR_DELETE_ELEMENT(callbacks, i, nCallbacks) < 0) {
            errno = EINVAL;
            return -1;
        }
    }

    return 0;
}


/*
 * PCI Device functions
 */
static char *
pci_address_format(struct pciDeviceAddress const *addr)
{
    char *ret;

    ret = g_strdup_printf(ADDR_STR_FMT,
                          addr->domain, addr->bus,
                          addr->device, addr->function);
    return ret;
}

static int
pci_address_parse(struct pciDeviceAddress *addr,
                  const char *buf)
{
    if (sscanf(buf, ADDR_STR_FMT,
               &addr->domain, &addr->bus,
               &addr->device, &addr->function) != 4)
        return -1;
    return 0;
}


static char *
pci_device_get_path(const struct pciDevice *dev,
                    const char *file,
                    bool faked)
{
    char *ret = NULL;
    const char *prefix = "";
    g_autofree char *devid = NULL;

    if (faked)
        prefix = fakerootdir;

    if (!(devid = pci_address_format(&dev->addr)))
        return NULL;

    /* PCI devices really do live under /sys/devices/pciDDDD:BB
     * and then they are just symlinked to /sys/bus/pci/devices/
     */
    if (file) {
        ret = g_strdup_printf("%s/sys/devices/pci%04x:%02x/%s/%s",
                              prefix, dev->addr.domain, dev->addr.bus,
                              devid, file);
    } else {
        ret = g_strdup_printf("%s/sys/devices/pci%04x:%02x/%s",
                              prefix, dev->addr.domain, dev->addr.bus,
                              devid);
    }

    return ret;
}


static void
pci_device_create_iommu(const struct pciDevice *dev,
                        const char *devid)
{
    struct pciIommuGroup *iommuGroup;
    g_autofree char *iommuPath = NULL;
    char tmp[256];
    size_t i;

    iommuPath = g_strdup_printf("%s/sys/kernel/iommu_groups/%d/devices/",
                                fakerootdir, dev->iommuGroup);

    if (virFileMakePath(iommuPath) < 0)
        ABORT("Unable to create: %s", iommuPath);

    if (g_snprintf(tmp, sizeof(tmp),
                   "../../../../devices/pci%04x:%02x/%s",
                   dev->addr.domain, dev->addr.bus, devid) < 0) {
        ABORT("@tmp overflow");
    }

    make_symlink(iommuPath, devid, tmp);

    /* pci_device_create_iommu can be called more than one for the
     * same iommuGroup. Bail out here if the iommuGroup was already
     * created beforehand. */
    for (i = 0; i < npciIommuGroups; i++) {
        if (pciIommuGroups[i]->iommu == dev->iommuGroup)
            return;
    }

    if (VIR_ALLOC_QUIET(iommuGroup) < 0)
        ABORT_OOM();

    iommuGroup->iommu = dev->iommuGroup;
    iommuGroup->nDevicesBoundToVFIO = 0; /* No device bound to VFIO by default */

    if (VIR_APPEND_ELEMENT_QUIET(pciIommuGroups, npciIommuGroups,
                                 iommuGroup) < 0)
        ABORT_OOM();
}


static void
pci_device_new_from_stub(const struct pciDevice *data)
{
    struct pciDevice *dev;
    g_autofree char *devpath = NULL;
    g_autofree char *devsympath = NULL;
    g_autofree char *id = NULL;
    g_autofree char *devid = NULL;
    char *c;
    g_autofree char *configSrc = NULL;
    char tmp[256];
    struct stat sb;
    bool configSrcExists = false;

    if (!(devid = pci_address_format(&data->addr)))
        ABORT_OOM();

    id = g_strdup(devid);

    /* Replace ':' with '-' to create the config filename from the
     * device ID. The device ID cannot be used directly as filename
     * because it contains ':' and Windows does not allow ':' in
     * filenames. */
    c = strchr(id, ':');

    while (c) {
        *c = '-';
        c = strchr(c, ':');
    }

    if (VIR_ALLOC_QUIET(dev) < 0)
        ABORT_OOM();

    configSrc = g_strdup_printf("%s/virpcitestdata/%s.config", abs_srcdir, id);

    memcpy(dev, data, sizeof(*dev));

    if (!(devpath = pci_device_get_path(dev, NULL, true)))
        ABORT_OOM();

    if (virFileMakePath(devpath) < 0)
        ABORT("Unable to create: %s", devpath);

    if (stat(configSrc, &sb) == 0)
        configSrcExists = true;

    /* If there is a config file for the device within virpcitestdata dir,
     * symlink it. Otherwise create a dummy config file. */
    if (configSrcExists) {
        /* On success, copy @configSrc into the destination (a copy,
         * rather than a symlink, is required since we write into the
         * file, and parallel VPATH builds must not stomp on the
         * original; besides, 'make distcheck' requires the original
         * to be read-only */
        g_autofree char *buf = NULL;
        ssize_t len;

        if ((len = virFileReadAll(configSrc, 4096, &buf)) < 0)
            ABORT("Unable to read config file '%s'", configSrc);

        make_file(devpath, "config", buf, len);
    } else {
        /* If there's no config data in the virpcitestdata dir, create a dummy
         * config file */
        make_file(devpath, "config", "some dummy config", -1);
    }

    if (g_snprintf(tmp, sizeof(tmp),  "0x%.4x", dev->vendor) < 0)
        ABORT("@tmp overflow");
    make_file(devpath, "vendor", tmp, -1);

    if (g_snprintf(tmp, sizeof(tmp),  "0x%.4x", dev->device) < 0)
        ABORT("@tmp overflow");
    make_file(devpath, "device", tmp, -1);

    if (g_snprintf(tmp, sizeof(tmp),  "0x%.4x", dev->klass) < 0)
        ABORT("@tmp overflow");
    make_file(devpath, "class", tmp, -1);

    make_file(devpath, "driver_override", NULL, -1);

    pci_device_create_iommu(dev, devid);

    if (g_snprintf(tmp, sizeof(tmp),
                   "../../../kernel/iommu_groups/%d", dev->iommuGroup) < 0) {
        ABORT("@tmp overflow");
    }
    make_symlink(devpath, "iommu_group", tmp);

    if (g_snprintf(tmp, sizeof(tmp),
                   "../../../devices/pci%04x:%02x/%s",
                   dev->addr.domain, dev->addr.bus, devid) < 0) {
        ABORT("@tmp overflow");
    }

    devsympath = g_strdup_printf("%s" SYSFS_PCI_PREFIX "devices", fakerootdir);

    make_symlink(devsympath, devid, tmp);

    if (dev->physfn) {
        if (g_snprintf(tmp, sizeof(tmp),
                       "%s%s/devices/%s", fakerootdir,
                       SYSFS_PCI_PREFIX, dev->physfn) < 0) {
            ABORT("@tmp overflow");
        }
        make_symlink(devpath, "physfn", tmp);
    }

    if (pci_device_autobind(dev) < 0)
        ABORT("Unable to bind: %s", devid);

    if (VIR_APPEND_ELEMENT_QUIET(pciDevices, nPCIDevices, dev) < 0)
        ABORT_OOM();
}

static struct pciDevice *
pci_device_find_by_id(struct pciDeviceAddress const *addr)
{
    size_t i;
    for (i = 0; i < nPCIDevices; i++) {
        struct pciDevice *dev = pciDevices[i];

        if (!memcmp(&dev->addr, addr, sizeof(*addr)))
            return dev;
    }

    return NULL;
}

static struct pciDevice *
pci_device_find_by_content(const char *path)
{
    char tmp[32];
    struct pciDeviceAddress addr;

    if (pci_read_file(path, tmp, sizeof(tmp), true) < 0 ||
        pci_address_parse(&addr, tmp) < 0)
        return NULL;

    return pci_device_find_by_id(&addr);
}

static int
pci_device_autobind(struct pciDevice *dev)
{
    struct pciDriver *driver = pci_driver_find_by_driver_override(dev);

    if (!driver)
        driver = pci_driver_find_by_dev(dev);

    if (!driver) {
        /* No driver found. Nothing to do */
        return 0;
    }

    return pci_driver_bind(driver, dev);
}

static int
pci_vfio_release_iommu(struct pciDevice *device)
{
    g_autofree char *vfiopath = NULL;
    size_t i = 0;

    for (i = 0; i < npciIommuGroups; i++) {
        if (device->iommuGroup != pciIommuGroups[i]->iommu)
            continue;

        if (pciIommuGroups[i]->nDevicesBoundToVFIO == 0) {
            errno = EXDEV;
            return -1;
        }

        pciIommuGroups[i]->nDevicesBoundToVFIO--;

        if (!pciIommuGroups[i]->nDevicesBoundToVFIO) {
            vfiopath = g_strdup_printf("%s/dev/vfio/%d",
                                       fakerootdir,
                                       device->iommuGroup);

            if (unlink(vfiopath) < 0)
                return -1;
        }
        break;
    }

    return 0;
}

static int
pci_vfio_lock_iommu(struct pciDevice *device)
{
    g_autofree char *vfiopath = NULL;
    int ret = -1;
    size_t i = 0;
    int fd = -1;

    for (i = 0; i < npciIommuGroups; i++) {
        if (device->iommuGroup != pciIommuGroups[i]->iommu)
            continue;

        if (pciIommuGroups[i]->nDevicesBoundToVFIO == 0) {
            vfiopath = g_strdup_printf("%s/dev/vfio/%d",
                                       fakerootdir,
                                       device->iommuGroup);
            if ((fd = real_open(vfiopath, O_CREAT)) < 0)
                goto cleanup;

        }

        pciIommuGroups[i]->nDevicesBoundToVFIO++;
        break;
    }

    ret = 0;
 cleanup:
    if (fd != -1)
        real_close(fd);
    return ret;
}

/*
 * PCI Driver functions
 */
static char *
pci_driver_get_path(const struct pciDriver *driver,
                    const char *file,
                    bool faked)
{
    char *ret = NULL;
    const char *prefix = "";

    if (faked)
        prefix = fakerootdir;

    if (file) {
        ret = g_strdup_printf("%s" SYSFS_PCI_PREFIX "drivers/%s/%s",
                              prefix, driver->name, file);
    } else {
        ret = g_strdup_printf("%s" SYSFS_PCI_PREFIX "drivers/%s",
                              prefix, driver->name);
    }

    return ret;
}


static void
pci_driver_new(const char *name, ...)
{
    struct pciDriver *driver;
    va_list args;
    int vendor, device;
    g_autofree char *driverpath = NULL;

    if (VIR_ALLOC_QUIET(driver) < 0)
        ABORT_OOM();
    driver->name = g_strdup(name);
    if (!(driverpath = pci_driver_get_path(driver, NULL, true)))
        ABORT_OOM();

    if (virFileMakePath(driverpath) < 0)
        ABORT("Unable to create: %s", driverpath);

    va_start(args, name);

    while ((vendor = va_arg(args, int)) != -1) {
        if ((device = va_arg(args, int)) == -1)
            ABORT("Invalid vendor device pair for driver %s", name);

        if (VIR_REALLOC_N_QUIET(driver->vendor, driver->len + 1) < 0 ||
            VIR_REALLOC_N_QUIET(driver->device, driver->len + 1) < 0)
            ABORT_OOM();

        driver->vendor[driver->len] = vendor;
        driver->device[driver->len] = device;
        driver->len++;
    }

    va_end(args);

    make_file(driverpath, "bind", NULL, -1);
    make_file(driverpath, "unbind", NULL, -1);

    if (VIR_APPEND_ELEMENT_QUIET(pciDrivers, nPCIDrivers, driver) < 0)
        ABORT_OOM();
}

static struct pciDriver *
pci_driver_find_by_dev(struct pciDevice *dev)
{
    size_t i;

    for (i = 0; i < nPCIDrivers; i++) {
        struct pciDriver *driver = pciDrivers[i];
        size_t j;

        for (j = 0; j < driver->len; j++) {
            if (driver->vendor[j] == dev->vendor &&
                driver->device[j] == dev->device)
                return driver;
        }
    }

    return NULL;
}

static struct pciDriver *
pci_driver_find_by_path(const char *path)
{
    size_t i;

    for (i = 0; i < nPCIDrivers; i++) {
        struct pciDriver *driver = pciDrivers[i];

        if (strstr(path, driver->name))
            return driver;
    }

    return NULL;
}

static struct pciDriver *
pci_driver_find_by_driver_override(struct pciDevice *dev)
{
    g_autofree char *path = NULL;
    char tmp[32];
    size_t i;

    if (!(path = pci_device_get_path(dev, "driver_override", false)))
        return NULL;

    if (pci_read_file(path, tmp, sizeof(tmp), false) < 0)
        return NULL;

    for (i = 0; i < nPCIDrivers; i++) {
        struct pciDriver *driver = pciDrivers[i];

        if (STREQ(tmp, driver->name))
            return driver;
    }

    return NULL;
}

static int
pci_driver_bind(struct pciDriver *driver,
                struct pciDevice *dev)
{
    g_autofree char *devid = NULL;
    g_autofree char *devpath = NULL;
    g_autofree char *driverpath = NULL;

    if (dev->driver) {
        /* Device already bound */
        errno = ENODEV;
        return -1;
    }

    /* Make symlink under device tree */
    if (!(devpath = pci_device_get_path(dev, "driver", true)) ||
        !(driverpath = pci_driver_get_path(driver, NULL, true))) {
        errno = ENOMEM;
        return -1;
    }

    if (symlink(driverpath, devpath) < 0)
        return -1;

    /* Make symlink under driver tree */
    VIR_FREE(devpath);
    VIR_FREE(driverpath);
    if (!(devid = pci_address_format(&dev->addr)) ||
        !(devpath = pci_device_get_path(dev, NULL, true)) ||
        !(driverpath = pci_driver_get_path(driver, devid, true))) {
        errno = ENOMEM;
        return -1;
    }

    if (symlink(devpath, driverpath) < 0)
        return -1;

    if (STREQ(driver->name, "vfio-pci") &&
        pci_vfio_lock_iommu(dev) < 0)
        return -1;

    dev->driver = driver;
    return 0;
}

static int
pci_driver_unbind(struct pciDriver *driver,
                  struct pciDevice *dev)
{
    g_autofree char *devid = NULL;
    g_autofree char *devpath = NULL;
    g_autofree char *driverpath = NULL;

    if (dev->driver != driver) {
        /* Device not bound to the @driver */
        errno = ENODEV;
        return -1;
    }

    /* Make symlink under device tree */
    if (!(devid = pci_address_format(&dev->addr)) ||
        !(devpath = pci_device_get_path(dev, "driver", true)) ||
        !(driverpath = pci_driver_get_path(driver, devid, true))) {
        errno = ENOMEM;
        return -1;
    }

    if (unlink(devpath) < 0 ||
        unlink(driverpath) < 0)
        return -1;

    if (STREQ(driver->name, "vfio-pci") &&
        pci_vfio_release_iommu(dev) < 0)
        return -1;

    dev->driver = NULL;
    return 0;
}

static int
pci_driver_handle_drivers_probe(const char *path)
{
    struct pciDevice *dev;

    if (!(dev = pci_device_find_by_content(path))) {
        errno = ENODEV;
        return -1;
    }

    if (dev->driver)
        return 0;

    return pci_device_autobind(dev);
}

static int
pci_driver_handle_change(int fd G_GNUC_UNUSED, const char *path)
{
    int ret;
    g_autofree char *file = g_path_get_basename(path);

    if (STREQ(file, "bind"))
        ret = pci_driver_handle_bind(path);
    else if (STREQ(file, "unbind"))
        ret = pci_driver_handle_unbind(path);
    else if (STREQ(file, "drivers_probe"))
        ret = pci_driver_handle_drivers_probe(path);
    else if (STREQ(file, "driver_override"))
        ret = 0; /* nada */
    else
        ABORT("Not handled write to: %s", path);
    return ret;
}

static int
pci_driver_handle_bind(const char *path)
{
    struct pciDevice *dev = pci_device_find_by_content(path);
    struct pciDriver *driver = pci_driver_find_by_path(path);

    if (!driver || !dev) {
        /* No driver, no device or failing driver requested */
        errno = ENODEV;
        return -1;
    }

    return pci_driver_bind(driver, dev);
}

static int
pci_driver_handle_unbind(const char *path)
{
    struct pciDevice *dev = pci_device_find_by_content(path);

    if (!dev || !dev->driver) {
        /* No device, device not binded or failing driver requested */
        errno = ENODEV;
        return -1;
    }

    return pci_driver_unbind(dev->driver, dev);
}


/*
 * Functions to load the symbols and init the environment
 */
static void
init_syms(void)
{
    if (real_access)
        return;

    VIR_MOCK_REAL_INIT(access);
    VIR_MOCK_REAL_INIT(open);
# ifdef __GLIBC__
    VIR_MOCK_REAL_INIT(__open_2);
# endif /* ! __GLIBC__ */
    VIR_MOCK_REAL_INIT(close);
    VIR_MOCK_REAL_INIT(opendir);
    VIR_MOCK_REAL_INIT(virFileCanonicalizePath);
}

static void
init_env(void)
{
    g_autofree char *tmp = NULL;

    if (!(fakerootdir = getenv("LIBVIRT_FAKE_ROOT_DIR")))
        ABORT("Missing LIBVIRT_FAKE_ROOT_DIR env variable\n");

    tmp = g_strdup_printf("%s%s", fakerootdir, SYSFS_PCI_PREFIX);

    if (virFileMakePath(tmp) < 0)
        ABORT("Unable to create: %s", tmp);

    make_dir(tmp, "devices");
    make_dir(tmp, "drivers");
    make_file(tmp, "drivers_probe", NULL, -1);

    /* Create /dev/vfio/ dir and /dev/vfio/vfio file */
    VIR_FREE(tmp);
    tmp = g_strdup_printf("%s/dev/vfio", fakerootdir);

    if (virFileMakePath(tmp) < 0)
        ABORT("Unable to create: %s", tmp);

    make_file(tmp, "vfio", NULL, -1);

# define MAKE_PCI_DRIVER(name, ...) \
    pci_driver_new(name, __VA_ARGS__, -1, -1)

    MAKE_PCI_DRIVER("iwlwifi", 0x8086, 0x0044);
    MAKE_PCI_DRIVER("i915", 0x8086, 0x0046, 0x8086, 0x0047);
    MAKE_PCI_DRIVER("vfio-pci", -1, -1);
    MAKE_PCI_DRIVER("nvme", 0x1cc1, 0x8201);

# define MAKE_PCI_DEVICE(Id, Vendor, Device, IommuGroup, ...) \
    do { \
        struct pciDevice dev = {.vendor = Vendor, \
                                .device = Device, \
                                .iommuGroup = IommuGroup, __VA_ARGS__}; \
        if (pci_address_parse(&dev.addr, Id) < 0) \
            ABORT("Unable to parse PCI address " Id); \
        pci_device_new_from_stub(&dev); \
    } while (0)

    MAKE_PCI_DEVICE("0000:00:00.0", 0x8086, 0x0044, 0);
    MAKE_PCI_DEVICE("0000:00:01.0", 0x8086, 0x0044, 1);
    MAKE_PCI_DEVICE("0000:00:02.0", 0x8086, 0x0046, 2);
    MAKE_PCI_DEVICE("0000:00:03.0", 0x8086, 0x0048, 3);
    MAKE_PCI_DEVICE("0001:00:00.0", 0x1014, 0x03b9, 4, .klass = 0x060400);
    MAKE_PCI_DEVICE("0001:01:00.0", 0x8086, 0x105e, 5);
    MAKE_PCI_DEVICE("0001:01:00.1", 0x8086, 0x105e, 5);
    MAKE_PCI_DEVICE("0005:80:00.0", 0x10b5, 0x8112, 6, .klass = 0x060400);
    MAKE_PCI_DEVICE("0005:90:01.0", 0x1033, 0x0035, 7);
    MAKE_PCI_DEVICE("0005:90:01.1", 0x1033, 0x0035, 7);
    MAKE_PCI_DEVICE("0005:90:01.2", 0x1033, 0x00e0, 7);
    MAKE_PCI_DEVICE("0005:90:01.3", 0x1033, 0x00e0, 7);
    MAKE_PCI_DEVICE("0000:0a:01.0", 0x8086, 0x0047, 8);
    MAKE_PCI_DEVICE("0000:0a:02.0", 0x8286, 0x0048, 8);
    MAKE_PCI_DEVICE("0000:0a:03.0", 0x8386, 0x0048, 8);
    MAKE_PCI_DEVICE("0000:06:12.0", 0x8086, 0x0047, 9);
    MAKE_PCI_DEVICE("0000:06:12.1", 0x8086, 0x0047, 10,
                    .physfn = "0000:06:12.0"); /* Virtual Function */
    MAKE_PCI_DEVICE("0000:06:12.2", 0x8086, 0x0047, 11,
                    .physfn = "0000:06:12.0"); /* Virtual Function */
    MAKE_PCI_DEVICE("0021:de:1f.0", 0x8086, 0x0047, 12);
    MAKE_PCI_DEVICE("0021:de:1f.1", 0x8086, 0x0047, 13,
                    .physfn = "0021:de:1f.0"); /* Virtual Function */

    MAKE_PCI_DEVICE("0000:01:00.0", 0x1cc1, 0x8201, 14, .klass = 0x010802);
    MAKE_PCI_DEVICE("0000:02:00.0", 0x1cc1, 0x8201, 15, .klass = 0x010802);
}


/*
 *
 * Mocked functions
 *
 */

int
access(const char *path, int mode)
{
    g_autofree char *newpath = NULL;

    init_syms();

    if (getrealpath(&newpath, path) < 0)
        return -1;

    return real_access(newpath, mode);
}


static int
virMockStatRedirect(const char *path, char **newpath)
{
    if (getrealpath(newpath, path) < 0)
        return -1;

    return 0;
}


int
open(const char *path, int flags, ...)
{
    int ret;
    g_autofree char *newpath = NULL;

    init_syms();

    if (getrealpath(&newpath, path) < 0)
        return -1;

    if (flags & O_CREAT) {
        va_list ap;
        mode_t mode;
        va_start(ap, flags);
        mode = (mode_t) va_arg(ap, int);
        va_end(ap);
        ret = real_open(newpath, flags, mode);
    } else {
        ret = real_open(newpath, flags);
    }

    /* Catch both: /sys/bus/pci/drivers/... and
     * /sys/bus/pci/device/.../driver/... */
    if (ret >= 0 && STRPREFIX(path, SYSFS_PCI_PREFIX) &&
        strstr(path, "driver") && add_fd(ret, path) < 0) {
        real_close(ret);
        ret = -1;
    }

    return ret;
}


# ifdef __GLIBC__
/* in some cases this function may not be present in headers, so we need
 * a declaration to silence the complier */
int
__open_2(const char *path, int flags);

int
__open_2(const char *path, int flags)
{
    g_autofree char *newpath = NULL;
    int ret;

    init_syms();

    if (getrealpath(&newpath, path) < 0)
        return -1;

    ret = real___open_2(newpath, flags);

    /* Catch both: /sys/bus/pci/drivers/... and
     * /sys/bus/pci/device/.../driver/... */
    if (ret >= 0 && STRPREFIX(path, SYSFS_PCI_PREFIX) &&
        strstr(path, "driver") && add_fd(ret, path) < 0) {
        real_close(ret);
        ret = -1;
    }

    return ret;
}
# endif /* ! __GLIBC__ */

DIR *
opendir(const char *path)
{
    g_autofree char *newpath = NULL;

    init_syms();

    if (getrealpath(&newpath, path) < 0)
        return NULL;

    return real_opendir(newpath);
}

int
close(int fd)
{
    if (remove_fd(fd) < 0)
        return -1;
    return real_close(fd);
}

char *
virFileCanonicalizePath(const char *path)
{
    g_autofree char *newpath = NULL;

    init_syms();

    if (getrealpath(&newpath, path) < 0)
        return NULL;

    return real_virFileCanonicalizePath(newpath);
}

# include "virmockstathelpers.c"

#else
/* Nothing to override on this platform */
#endif
