/* -*- Mode: C; indent-tabs-mode:nil -*- */
/*
 * darwin backend for libusb 1.0
 * Copyright © 2008-2020 Nathan Hjelm <hjelmn@cs.unm.edu>
 * Copyright © 2019-2020 Google LLC. All rights reserved.
 *
 * 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, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 */

#include <config.h>
#include <assert.h>
#include <time.h>
#include <ctype.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/sysctl.h>

#include <mach/clock.h>
#include <mach/clock_types.h>
#include <mach/mach_host.h>
#include <mach/mach_port.h>

/* Suppress warnings about the use of the deprecated objc_registerThreadWithCollector
 * function. Its use is also conditionalized to only older deployment targets. */
#define OBJC_SILENCE_GC_DEPRECATIONS 1

#include <AvailabilityMacros.h>
#if MAC_OS_X_VERSION_MIN_REQUIRED >= 1060 && MAC_OS_X_VERSION_MIN_REQUIRED < 101200
  #include <objc/objc-auto.h>
#endif

#include "darwin_usb.h"

static pthread_mutex_t libusb_darwin_init_mutex = PTHREAD_MUTEX_INITIALIZER;
static int init_count = 0;

/* async event thread */
static pthread_mutex_t libusb_darwin_at_mutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t  libusb_darwin_at_cond = PTHREAD_COND_INITIALIZER;

#if !defined(HAVE_CLOCK_GETTIME)
static clock_serv_t clock_realtime;
static clock_serv_t clock_monotonic;
#endif

#define LIBUSB_DARWIN_STARTUP_FAILURE ((CFRunLoopRef) -1)

static CFRunLoopRef libusb_darwin_acfl = NULL; /* event cf loop */
static CFRunLoopSourceRef libusb_darwin_acfls = NULL; /* shutdown signal for event cf loop */

static usbi_mutex_t darwin_cached_devices_lock = PTHREAD_MUTEX_INITIALIZER;
static struct list_head darwin_cached_devices;
static const char *darwin_device_class = "IOUSBDevice";

#define DARWIN_CACHED_DEVICE(a) (((struct darwin_device_priv *)usbi_get_device_priv((a)))->dev)

/* async event thread */
static pthread_t libusb_darwin_at;

static int darwin_get_config_descriptor(struct libusb_device *dev, uint8_t config_index, void *buffer, size_t len);
static int darwin_claim_interface(struct libusb_device_handle *dev_handle, uint8_t iface);
static int darwin_release_interface(struct libusb_device_handle *dev_handle, uint8_t iface);
static int darwin_reset_device(struct libusb_device_handle *dev_handle);
static void darwin_async_io_callback (void *refcon, IOReturn result, void *arg0);

static enum libusb_error darwin_scan_devices(struct libusb_context *ctx);
static enum libusb_error process_new_device (struct libusb_context *ctx, struct darwin_cached_device *cached_device,
                                             UInt64 old_session_id);

static enum libusb_error darwin_get_cached_device(io_service_t service, struct darwin_cached_device **cached_out,
                                                  UInt64 *old_session_id);

#if defined(ENABLE_LOGGING)
static const char *darwin_error_str (IOReturn result) {
  static char string_buffer[50];
  switch (result) {
  case kIOReturnSuccess:
    return "no error";
  case kIOReturnNotOpen:
    return "device not opened for exclusive access";
  case kIOReturnNoDevice:
    return "no connection to an IOService";
  case kIOUSBNoAsyncPortErr:
    return "no async port has been opened for interface";
  case kIOReturnExclusiveAccess:
    return "another process has device opened for exclusive access";
  case kIOUSBPipeStalled:
    return "pipe is stalled";
  case kIOReturnError:
    return "could not establish a connection to the Darwin kernel";
  case kIOUSBTransactionTimeout:
    return "transaction timed out";
  case kIOReturnBadArgument:
    return "invalid argument";
  case kIOReturnAborted:
    return "transaction aborted";
  case kIOReturnNotResponding:
    return "device not responding";
  case kIOReturnOverrun:
    return "data overrun";
  case kIOReturnCannotWire:
    return "physical memory can not be wired down";
  case kIOReturnNoResources:
    return "out of resources";
  case kIOUSBHighSpeedSplitError:
    return "high speed split error";
  case kIOUSBUnknownPipeErr:
    return "pipe ref not recognized";
  default:
    snprintf(string_buffer, sizeof(string_buffer), "unknown error (0x%x)", result);
    return string_buffer;
  }
}
#endif

static enum libusb_error darwin_to_libusb (IOReturn result) {
  switch (result) {
  case kIOReturnUnderrun:
  case kIOReturnSuccess:
    return LIBUSB_SUCCESS;
  case kIOReturnNotOpen:
  case kIOReturnNoDevice:
    return LIBUSB_ERROR_NO_DEVICE;
  case kIOReturnExclusiveAccess:
    return LIBUSB_ERROR_ACCESS;
  case kIOUSBPipeStalled:
    return LIBUSB_ERROR_PIPE;
  case kIOReturnBadArgument:
    return LIBUSB_ERROR_INVALID_PARAM;
  case kIOUSBTransactionTimeout:
    return LIBUSB_ERROR_TIMEOUT;
  case kIOReturnNotResponding:
  case kIOReturnAborted:
  case kIOReturnError:
  case kIOUSBNoAsyncPortErr:
  case kIOUSBUnknownPipeErr:
  default:
    return LIBUSB_ERROR_OTHER;
  }
}

/* this function must be called with the darwin_cached_devices_lock held */
static void darwin_deref_cached_device(struct darwin_cached_device *cached_dev) {
  cached_dev->refcount--;
  /* free the device and remove it from the cache */
  if (0 == cached_dev->refcount) {
    list_del(&cached_dev->list);

    if (cached_dev->device) {
      (*(cached_dev->device))->Release(cached_dev->device);
      cached_dev->device = NULL;
    }
    free (cached_dev);
  }
}

static void darwin_ref_cached_device(struct darwin_cached_device *cached_dev) {
  cached_dev->refcount++;
}

static int ep_to_pipeRef(struct libusb_device_handle *dev_handle, uint8_t ep, uint8_t *pipep, uint8_t *ifcp, struct darwin_interface **interface_out) {
  struct darwin_device_handle_priv *priv = usbi_get_device_handle_priv(dev_handle);

  /* current interface */
  struct darwin_interface *cInterface;

  uint8_t i, iface;

  usbi_dbg ("converting ep address 0x%02x to pipeRef and interface", ep);

  for (iface = 0 ; iface < USB_MAXINTERFACES ; iface++) {
    cInterface = &priv->interfaces[iface];

    if (dev_handle->claimed_interfaces & (1U << iface)) {
      for (i = 0 ; i < cInterface->num_endpoints ; i++) {
        if (cInterface->endpoint_addrs[i] == ep) {
          *pipep = i + 1;

          if (ifcp)
            *ifcp = iface;

          if (interface_out)
            *interface_out = cInterface;

          usbi_dbg ("pipe %d on interface %d matches", *pipep, iface);
          return LIBUSB_SUCCESS;
        }
      }
    }
  }

  /* No pipe found with the correct endpoint address */
  usbi_warn (HANDLE_CTX(dev_handle), "no pipeRef found with endpoint address 0x%02x.", ep);

  return LIBUSB_ERROR_NOT_FOUND;
}

static IOReturn usb_setup_device_iterator (io_iterator_t *deviceIterator, UInt32 location) {
  CFMutableDictionaryRef matchingDict = IOServiceMatching(darwin_device_class);

  if (!matchingDict)
    return kIOReturnError;

  if (location) {
    CFMutableDictionaryRef propertyMatchDict = CFDictionaryCreateMutable(kCFAllocatorDefault, 0,
                                                                         &kCFTypeDictionaryKeyCallBacks,
                                                                         &kCFTypeDictionaryValueCallBacks);

    /* there are no unsigned CFNumber types so treat the value as signed. the OS seems to do this
         internally (CFNumberType of locationID is kCFNumberSInt32Type) */
    CFTypeRef locationCF = CFNumberCreate (NULL, kCFNumberSInt32Type, &location);

    if (propertyMatchDict && locationCF) {
      CFDictionarySetValue (propertyMatchDict, CFSTR(kUSBDevicePropertyLocationID), locationCF);
      CFDictionarySetValue (matchingDict, CFSTR(kIOPropertyMatchKey), propertyMatchDict);
    }
    /* else we can still proceed as long as the caller accounts for the possibility of other devices in the iterator */

    /* release our references as per the Create Rule */
    if (propertyMatchDict)
      CFRelease (propertyMatchDict);
    if (locationCF)
      CFRelease (locationCF);
  }

  return IOServiceGetMatchingServices(kIOMasterPortDefault, matchingDict, deviceIterator);
}

/* Returns 1 on success, 0 on failure. */
static bool get_ioregistry_value_number (io_service_t service, CFStringRef property, CFNumberType type, void *p) {
  CFTypeRef cfNumber = IORegistryEntryCreateCFProperty (service, property, kCFAllocatorDefault, 0);
  Boolean success = 0;

  if (cfNumber) {
    if (CFGetTypeID(cfNumber) == CFNumberGetTypeID()) {
      success = CFNumberGetValue(cfNumber, type, p);
    }

    CFRelease (cfNumber);
  }

  return (success != 0);
}

/* Returns 1 on success, 0 on failure. */
static bool get_ioregistry_value_data (io_service_t service, CFStringRef property, ssize_t size, void *p) {
  CFTypeRef cfData = IORegistryEntryCreateCFProperty (service, property, kCFAllocatorDefault, 0);
  bool success = false;

  if (cfData) {
    if (CFGetTypeID (cfData) == CFDataGetTypeID ()) {
      CFIndex length = CFDataGetLength (cfData);
      if (length < size) {
        size = length;
      }

      CFDataGetBytes (cfData, CFRangeMake(0, size), p);
      success = true;
    }

    CFRelease (cfData);
  }

  return success;
}

static usb_device_t **darwin_device_from_service (io_service_t service)
{
  io_cf_plugin_ref_t *plugInInterface = NULL;
  usb_device_t **device;
  IOReturn kresult;
  SInt32 score;
  const int max_retries = 5;

  /* The IOCreatePlugInInterfaceForService function might consistently return
     an "out of resources" error with certain USB devices the first time we run 
     it. The reason is still unclear, but retrying fixes the problem */
  for (int count = 0; count < max_retries; count++) {
    kresult = IOCreatePlugInInterfaceForService(service, kIOUSBDeviceUserClientTypeID,
                                                kIOCFPlugInInterfaceID, &plugInInterface,
                                                &score);
    if (kIOReturnSuccess == kresult && plugInInterface) {
      break;
    }

    usbi_dbg ("set up plugin for service retry: %s", darwin_error_str (kresult));

    /* sleep for a little while before trying again */
    nanosleep(&(struct timespec){.tv_sec = 0, .tv_nsec = 1000}, NULL);
  }

  if (kIOReturnSuccess != kresult || !plugInInterface) {
    usbi_dbg ("could not set up plugin for service: %s", darwin_error_str (kresult));
    return NULL;
  }

  (void)(*plugInInterface)->QueryInterface(plugInInterface, CFUUIDGetUUIDBytes(DeviceInterfaceID),
                                           (LPVOID)&device);
  /* Use release instead of IODestroyPlugInInterface to avoid stopping IOServices associated with this device */
  (*plugInInterface)->Release (plugInInterface);

  return device;
}

static void darwin_devices_attached (void *ptr, io_iterator_t add_devices) {
  UNUSED(ptr);
  struct darwin_cached_device *cached_device;
  UInt64 old_session_id;
  struct libusb_context *ctx;
  io_service_t service;
  int ret;

  usbi_mutex_lock(&active_contexts_lock);

  while ((service = IOIteratorNext(add_devices))) {
    ret = darwin_get_cached_device (service, &cached_device, &old_session_id);
    if (ret < 0 || !cached_device->can_enumerate) {
      continue;
    }

    /* add this device to each active context's device list */
    for_each_context(ctx) {
      process_new_device (ctx, cached_device, old_session_id);
    }

    if (cached_device->in_reenumerate) {
      usbi_dbg ("cached device in reset state. reset complete...");
      cached_device->in_reenumerate = false;
    }

    IOObjectRelease(service);
  }

  usbi_mutex_unlock(&active_contexts_lock);
}

static void darwin_devices_detached (void *ptr, io_iterator_t rem_devices) {
  UNUSED(ptr);
  struct libusb_device *dev = NULL;
  struct libusb_context *ctx;
  struct darwin_cached_device *old_device;

  io_service_t device;
  UInt64 session;
  int ret;

  usbi_mutex_lock(&active_contexts_lock);

  while ((device = IOIteratorNext (rem_devices)) != 0) {
    bool is_reenumerating = false;

    /* get the location from the i/o registry */
    ret = get_ioregistry_value_number (device, CFSTR("sessionID"), kCFNumberSInt64Type, &session);
    IOObjectRelease (device);
    if (!ret)
      continue;

    /* we need to match darwin_ref_cached_device call made in darwin_get_cached_device function
       otherwise no cached device will ever get freed */
    usbi_mutex_lock(&darwin_cached_devices_lock);
    list_for_each_entry(old_device, &darwin_cached_devices, list, struct darwin_cached_device) {
      if (old_device->session == session) {
        if (old_device->in_reenumerate) {
          /* device is re-enumerating. do not dereference the device at this time. libusb_reset_device()
           * will deref if needed. */
          usbi_dbg ("detected device detached due to re-enumeration");

          /* the device object is no longer usable so go ahead and release it */
          if (old_device->device) {
            (*(old_device->device))->Release(old_device->device);
            old_device->device = NULL;
          }

          is_reenumerating = true;
        } else {
          darwin_deref_cached_device (old_device);
        }

        break;
      }
    }

    usbi_mutex_unlock(&darwin_cached_devices_lock);
    if (is_reenumerating) {
      continue;
    }

    for_each_context(ctx) {
      usbi_dbg ("notifying context %p of device disconnect", ctx);

      dev = usbi_get_device_by_session_id(ctx, (unsigned long) session);
      if (dev) {
        /* signal the core that this device has been disconnected. the core will tear down this device
           when the reference count reaches 0 */
        usbi_disconnect_device(dev);
        libusb_unref_device(dev);
      }
    }
  }

  usbi_mutex_unlock(&active_contexts_lock);
}

static void darwin_hotplug_poll (void)
{
  /* not sure if 1 ms will be too long/short but it should work ok */
  mach_timespec_t timeout = {.tv_sec = 0, .tv_nsec = 1000000ul};

  /* since a kernel thread may notify the IOIterators used for
   * hotplug notification we can't just clear the iterators.
   * instead just wait until all IOService providers are quiet */
  (void) IOKitWaitQuiet (kIOMasterPortDefault, &timeout);
}

static void darwin_clear_iterator (io_iterator_t iter) {
  io_service_t device;

  while ((device = IOIteratorNext (iter)) != 0)
    IOObjectRelease (device);
}

static void darwin_fail_startup(void) {
  pthread_mutex_lock (&libusb_darwin_at_mutex);
  libusb_darwin_acfl = LIBUSB_DARWIN_STARTUP_FAILURE;
  pthread_cond_signal (&libusb_darwin_at_cond);
  pthread_mutex_unlock (&libusb_darwin_at_mutex);
  pthread_exit (NULL);
}

static void *darwin_event_thread_main (void *arg0) {
  IOReturn kresult;
  struct libusb_context *ctx = (struct libusb_context *)arg0;
  CFRunLoopRef runloop;
  CFRunLoopSourceRef libusb_shutdown_cfsource;
  CFRunLoopSourceContext libusb_shutdown_cfsourcectx;

#if MAC_OS_X_VERSION_MIN_REQUIRED >= 1060
  /* Set this thread's name, so it can be seen in the debugger
     and crash reports. */
  pthread_setname_np ("org.libusb.device-hotplug");
#endif

#if MAC_OS_X_VERSION_MIN_REQUIRED >= 1060 && MAC_OS_X_VERSION_MIN_REQUIRED < 101200
  /* Tell the Objective-C garbage collector about this thread.
     This is required because, unlike NSThreads, pthreads are
     not automatically registered. Although we don't use
     Objective-C, we use CoreFoundation, which does.
     Garbage collection support was entirely removed in 10.12,
     so don't bother there. */
  objc_registerThreadWithCollector();
#endif

  /* hotplug (device arrival/removal) sources */
  CFRunLoopSourceRef     libusb_notification_cfsource;
  io_notification_port_t libusb_notification_port;
  io_iterator_t          libusb_rem_device_iterator;
  io_iterator_t          libusb_add_device_iterator;

  usbi_dbg ("creating hotplug event source");

  runloop = CFRunLoopGetCurrent ();
  CFRetain (runloop);

  /* add the shutdown cfsource to the run loop */
  memset(&libusb_shutdown_cfsourcectx, 0, sizeof(libusb_shutdown_cfsourcectx));
  libusb_shutdown_cfsourcectx.info = runloop;
  libusb_shutdown_cfsourcectx.perform = (void (*)(void *))CFRunLoopStop;
  libusb_shutdown_cfsource = CFRunLoopSourceCreate(NULL, 0, &libusb_shutdown_cfsourcectx);
  CFRunLoopAddSource(runloop, libusb_shutdown_cfsource, kCFRunLoopDefaultMode);

  /* add the notification port to the run loop */
  libusb_notification_port     = IONotificationPortCreate (kIOMasterPortDefault);
  libusb_notification_cfsource = IONotificationPortGetRunLoopSource (libusb_notification_port);
  CFRunLoopAddSource(runloop, libusb_notification_cfsource, kCFRunLoopDefaultMode);

  /* create notifications for removed devices */
  kresult = IOServiceAddMatchingNotification (libusb_notification_port, kIOTerminatedNotification,
                                              IOServiceMatching(darwin_device_class),
                                              darwin_devices_detached,
                                              ctx, &libusb_rem_device_iterator);

  if (kresult != kIOReturnSuccess) {
    usbi_err (ctx, "could not add hotplug event source: %s", darwin_error_str (kresult));
    CFRelease (libusb_shutdown_cfsource);
    CFRelease (runloop);
    darwin_fail_startup ();
  }

  /* create notifications for attached devices */
  kresult = IOServiceAddMatchingNotification(libusb_notification_port, kIOFirstMatchNotification,
                                              IOServiceMatching(darwin_device_class),
                                              darwin_devices_attached,
                                              ctx, &libusb_add_device_iterator);

  if (kresult != kIOReturnSuccess) {
    usbi_err (ctx, "could not add hotplug event source: %s", darwin_error_str (kresult));
    CFRelease (libusb_shutdown_cfsource);
    CFRelease (runloop);
    darwin_fail_startup ();
  }

  /* arm notifiers */
  darwin_clear_iterator (libusb_rem_device_iterator);
  darwin_clear_iterator (libusb_add_device_iterator);

  usbi_dbg ("darwin event thread ready to receive events");

  /* signal the main thread that the hotplug runloop has been created. */
  pthread_mutex_lock (&libusb_darwin_at_mutex);
  libusb_darwin_acfl = runloop;
  libusb_darwin_acfls = libusb_shutdown_cfsource;
  pthread_cond_signal (&libusb_darwin_at_cond);
  pthread_mutex_unlock (&libusb_darwin_at_mutex);

  /* run the runloop */
  CFRunLoopRun();

  usbi_dbg ("darwin event thread exiting");

  /* signal the main thread that the hotplug runloop has finished. */
  pthread_mutex_lock (&libusb_darwin_at_mutex);
  libusb_darwin_acfls = NULL;
  libusb_darwin_acfl = NULL;
  pthread_cond_signal (&libusb_darwin_at_cond);
  pthread_mutex_unlock (&libusb_darwin_at_mutex);

  /* remove the notification cfsource */
  CFRunLoopRemoveSource(runloop, libusb_notification_cfsource, kCFRunLoopDefaultMode);

  /* remove the shutdown cfsource */
  CFRunLoopRemoveSource(runloop, libusb_shutdown_cfsource, kCFRunLoopDefaultMode);

  /* delete notification port */
  IONotificationPortDestroy (libusb_notification_port);

  /* delete iterators */
  IOObjectRelease (libusb_rem_device_iterator);
  IOObjectRelease (libusb_add_device_iterator);

  CFRelease (libusb_shutdown_cfsource);
  CFRelease (runloop);

  pthread_exit (NULL);
}

/* cleanup function to destroy cached devices */
static void darwin_cleanup_devices(void) {
  struct darwin_cached_device *dev, *next;

  list_for_each_entry_safe(dev, next, &darwin_cached_devices, list, struct darwin_cached_device) {
    darwin_deref_cached_device(dev);
  }

  darwin_cached_devices.prev = darwin_cached_devices.next = NULL;
}

static int darwin_init(struct libusb_context *ctx) {
  bool first_init;
  int rc;

  pthread_mutex_lock (&libusb_darwin_init_mutex);

  first_init = (1 == ++init_count);

  do {
    if (first_init) {
      assert (NULL == darwin_cached_devices.next);
      list_init (&darwin_cached_devices);

#if !defined(HAVE_CLOCK_GETTIME)
      /* create the clocks that will be used if clock_gettime() is not available */
      host_name_port_t host_self;

      host_self = mach_host_self();
      host_get_clock_service(host_self, CALENDAR_CLOCK, &clock_realtime);
      host_get_clock_service(host_self, SYSTEM_CLOCK, &clock_monotonic);
      mach_port_deallocate(mach_task_self(), host_self);
#endif
    }

    rc = darwin_scan_devices (ctx);
    if (LIBUSB_SUCCESS != rc)
      break;

    if (first_init) {
      rc = pthread_create (&libusb_darwin_at, NULL, darwin_event_thread_main, ctx);
      if (0 != rc) {
        usbi_err (ctx, "could not create event thread, error %d", rc);
        rc = LIBUSB_ERROR_OTHER;
        break;
      }

      pthread_mutex_lock (&libusb_darwin_at_mutex);
      while (!libusb_darwin_acfl)
        pthread_cond_wait (&libusb_darwin_at_cond, &libusb_darwin_at_mutex);
      if (libusb_darwin_acfl == LIBUSB_DARWIN_STARTUP_FAILURE) {
        libusb_darwin_acfl = NULL;
        rc = LIBUSB_ERROR_OTHER;
      }
      pthread_mutex_unlock (&libusb_darwin_at_mutex);

      if (0 != rc)
        pthread_join (libusb_darwin_at, NULL);
    }
  } while (0);

  if (LIBUSB_SUCCESS != rc) {
    if (first_init) {
      darwin_cleanup_devices ();
#if !defined(HAVE_CLOCK_GETTIME)
      mach_port_deallocate(mach_task_self(), clock_realtime);
      mach_port_deallocate(mach_task_self(), clock_monotonic);
#endif
    }
    --init_count;
  }

  pthread_mutex_unlock (&libusb_darwin_init_mutex);

  return rc;
}

static void darwin_exit (struct libusb_context *ctx) {
  UNUSED(ctx);

  pthread_mutex_lock (&libusb_darwin_init_mutex);

  if (0 == --init_count) {
    /* stop the event runloop and wait for the thread to terminate. */
    pthread_mutex_lock (&libusb_darwin_at_mutex);
    CFRunLoopSourceSignal (libusb_darwin_acfls);
    CFRunLoopWakeUp (libusb_darwin_acfl);
    while (libusb_darwin_acfl)
      pthread_cond_wait (&libusb_darwin_at_cond, &libusb_darwin_at_mutex);
    pthread_mutex_unlock (&libusb_darwin_at_mutex);
    pthread_join (libusb_darwin_at, NULL);

    darwin_cleanup_devices ();

#if !defined(HAVE_CLOCK_GETTIME)
    mach_port_deallocate(mach_task_self(), clock_realtime);
    mach_port_deallocate(mach_task_self(), clock_monotonic);
#endif
  }

  pthread_mutex_unlock (&libusb_darwin_init_mutex);
}

static int get_configuration_index (struct libusb_device *dev, UInt8 config_value) {
  struct darwin_cached_device *priv = DARWIN_CACHED_DEVICE(dev);
  UInt8 i, numConfig;
  IOUSBConfigurationDescriptorPtr desc;
  IOReturn kresult;

  /* is there a simpler way to determine the index? */
  kresult = (*(priv->device))->GetNumberOfConfigurations (priv->device, &numConfig);
  if (kresult != kIOReturnSuccess)
    return darwin_to_libusb (kresult);

  for (i = 0 ; i < numConfig ; i++) {
    (*(priv->device))->GetConfigurationDescriptorPtr (priv->device, i, &desc);

    if (desc->bConfigurationValue == config_value)
      return i;
  }

  /* configuration not found */
  return LIBUSB_ERROR_NOT_FOUND;
}

static int darwin_get_active_config_descriptor(struct libusb_device *dev, void *buffer, size_t len) {
  struct darwin_cached_device *priv = DARWIN_CACHED_DEVICE(dev);
  int config_index;

  if (0 == priv->active_config)
    return LIBUSB_ERROR_NOT_FOUND;

  config_index = get_configuration_index (dev, priv->active_config);
  if (config_index < 0)
    return config_index;

  assert(config_index >= 0 && config_index <= UINT8_MAX);
  return darwin_get_config_descriptor (dev, (UInt8)config_index, buffer, len);
}

static int darwin_get_config_descriptor(struct libusb_device *dev, uint8_t config_index, void *buffer, size_t len) {
  struct darwin_cached_device *priv = DARWIN_CACHED_DEVICE(dev);
  IOUSBConfigurationDescriptorPtr desc;
  IOReturn kresult;
  int ret;

  if (!priv || !priv->device)
    return LIBUSB_ERROR_OTHER;

  kresult = (*priv->device)->GetConfigurationDescriptorPtr (priv->device, config_index, &desc);
  if (kresult == kIOReturnSuccess) {
    /* copy descriptor */
    if (libusb_le16_to_cpu(desc->wTotalLength) < len)
      len = libusb_le16_to_cpu(desc->wTotalLength);

    memmove (buffer, desc, len);
  }

  ret = darwin_to_libusb (kresult);
  if (ret != LIBUSB_SUCCESS)
    return ret;

  return (int) len;
}

/* check whether the os has configured the device */
static enum libusb_error darwin_check_configuration (struct libusb_context *ctx, struct darwin_cached_device *dev) {
  usb_device_t **darwin_device = dev->device;

  IOUSBConfigurationDescriptorPtr configDesc;
  IOUSBFindInterfaceRequest request;
  IOReturn                  kresult;
  io_iterator_t             interface_iterator;
  io_service_t              firstInterface;

  if (dev->dev_descriptor.bNumConfigurations < 1) {
    usbi_err (ctx, "device has no configurations");
    return LIBUSB_ERROR_OTHER; /* no configurations at this speed so we can't use it */
  }

  /* checking the configuration of a root hub simulation takes ~1 s in 10.11. the device is
     not usable anyway */
  if (0x05ac == libusb_le16_to_cpu (dev->dev_descriptor.idVendor) &&
      0x8005 == libusb_le16_to_cpu (dev->dev_descriptor.idProduct)) {
    usbi_dbg ("ignoring configuration on root hub simulation");
    dev->active_config = 0;
    return LIBUSB_SUCCESS;
  }

  /* find the first configuration */
  kresult = (*darwin_device)->GetConfigurationDescriptorPtr (darwin_device, 0, &configDesc);
  dev->first_config = (kIOReturnSuccess == kresult) ? configDesc->bConfigurationValue : 1;

  /* check if the device is already configured. there is probably a better way than iterating over the
     to accomplish this (the trick is we need to avoid a call to GetConfigurations since buggy devices
     might lock up on the device request) */

  /* Setup the Interface Request */
  request.bInterfaceClass    = kIOUSBFindInterfaceDontCare;
  request.bInterfaceSubClass = kIOUSBFindInterfaceDontCare;
  request.bInterfaceProtocol = kIOUSBFindInterfaceDontCare;
  request.bAlternateSetting  = kIOUSBFindInterfaceDontCare;

  kresult = (*(darwin_device))->CreateInterfaceIterator(darwin_device, &request, &interface_iterator);
  if (kresult != kIOReturnSuccess)
    return darwin_to_libusb (kresult);

  /* iterate once */
  firstInterface = IOIteratorNext(interface_iterator);

  /* done with the interface iterator */
  IOObjectRelease(interface_iterator);

  if (firstInterface) {
    IOObjectRelease (firstInterface);

    /* device is configured */
    if (dev->dev_descriptor.bNumConfigurations == 1)
      /* to avoid problems with some devices get the configurations value from the configuration descriptor */
      dev->active_config = dev->first_config;
    else
      /* devices with more than one configuration should work with GetConfiguration */
      (*darwin_device)->GetConfiguration (darwin_device, &dev->active_config);
  } else
    /* not configured */
    dev->active_config = 0;

  usbi_dbg ("active config: %u, first config: %u", dev->active_config, dev->first_config);

  return LIBUSB_SUCCESS;
}

static IOReturn darwin_request_descriptor (usb_device_t **device, UInt8 desc, UInt8 desc_index, void *buffer, size_t buffer_size) {
  IOUSBDevRequestTO req;

  assert(buffer_size <= UINT16_MAX);

  memset (buffer, 0, buffer_size);

  /* Set up request for descriptor/ */
  req.bmRequestType = USBmakebmRequestType(kUSBIn, kUSBStandard, kUSBDevice);
  req.bRequest      = kUSBRqGetDescriptor;
  req.wValue        = (UInt16)(desc << 8);
  req.wIndex        = desc_index;
  req.wLength       = (UInt16)buffer_size;
  req.pData         = buffer;
  req.noDataTimeout = 20;
  req.completionTimeout = 100;

  return (*device)->DeviceRequestTO (device, &req);
}

static enum libusb_error darwin_cache_device_descriptor (struct darwin_cached_device *dev) {
  usb_device_t **device = dev->device;
  int retries = 1;
  long delay = 30000; // microseconds
  int unsuspended = 0, try_unsuspend = 1, try_reconfigure = 1;
  int is_open = 0;
  IOReturn ret = 0, ret2;
  UInt8 bDeviceClass;
  UInt16 idProduct, idVendor;

  dev->can_enumerate = 0;

  (*device)->GetDeviceClass (device, &bDeviceClass);
  (*device)->GetDeviceProduct (device, &idProduct);
  (*device)->GetDeviceVendor (device, &idVendor);

  /* According to Apple's documentation the device must be open for DeviceRequest but we may not be able to open some
   * devices and Apple's USB Prober doesn't bother to open the device before issuing a descriptor request.  Still,
   * to follow the spec as closely as possible, try opening the device */
  is_open = ((*device)->USBDeviceOpenSeize(device) == kIOReturnSuccess);

  do {
    /**** retrieve device descriptor ****/
    ret = darwin_request_descriptor (device, kUSBDeviceDesc, 0, &dev->dev_descriptor, sizeof(dev->dev_descriptor));

    if (kIOReturnOverrun == ret && kUSBDeviceDesc == dev->dev_descriptor.bDescriptorType)
      /* received an overrun error but we still received a device descriptor */
      ret = kIOReturnSuccess;

    if (kIOUSBVendorIDAppleComputer == idVendor) {
      /* NTH: don't bother retrying or unsuspending Apple devices */
      break;
    }

    if (kIOReturnSuccess == ret && (0 == dev->dev_descriptor.bNumConfigurations ||
                                    0 == dev->dev_descriptor.bcdUSB)) {
      /* work around for incorrectly configured devices */
      if (try_reconfigure && is_open) {
        usbi_dbg("descriptor appears to be invalid. resetting configuration before trying again...");

        /* set the first configuration */
        (*device)->SetConfiguration(device, 1);

        /* don't try to reconfigure again */
        try_reconfigure = 0;
      }

      ret = kIOUSBPipeStalled;
    }

    if (kIOReturnSuccess != ret && is_open && try_unsuspend) {
      /* device may be suspended. unsuspend it and try again */
#if DeviceVersion >= 320
      UInt32 info = 0;

      /* IOUSBFamily 320+ provides a way to detect device suspension but earlier versions do not */
      (void)(*device)->GetUSBDeviceInformation (device, &info);

      /* note that the device was suspended */
      if (info & (1U << kUSBInformationDeviceIsSuspendedBit) || 0 == info)
        try_unsuspend = 1;
#endif

      if (try_unsuspend) {
        /* try to unsuspend the device */
        ret2 = (*device)->USBDeviceSuspend (device, 0);
        if (kIOReturnSuccess != ret2) {
          /* prevent log spew from poorly behaving devices.  this indicates the
             os actually had trouble communicating with the device */
          usbi_dbg("could not retrieve device descriptor. failed to unsuspend: %s",darwin_error_str(ret2));
        } else
          unsuspended = 1;

        try_unsuspend = 0;
      }
    }

    if (kIOReturnSuccess != ret) {
      usbi_dbg("kernel responded with code: 0x%08x. sleeping for %ld ms before trying again", ret, delay/1000);
      /* sleep for a little while before trying again */
      nanosleep(&(struct timespec){delay / 1000000, (delay * 1000) % 1000000000}, NULL);
    }
  } while (kIOReturnSuccess != ret && retries--);

  if (unsuspended)
    /* resuspend the device */
    (void)(*device)->USBDeviceSuspend (device, 1);

  if (is_open)
    (void) (*device)->USBDeviceClose (device);

  if (ret != kIOReturnSuccess) {
    /* a debug message was already printed out for this error */
    if (LIBUSB_CLASS_HUB == bDeviceClass)
      usbi_dbg ("could not retrieve device descriptor %.4x:%.4x: %s (%x). skipping device",
                idVendor, idProduct, darwin_error_str (ret), ret);
    else
      usbi_warn (NULL, "could not retrieve device descriptor %.4x:%.4x: %s (%x). skipping device",
                 idVendor, idProduct, darwin_error_str (ret), ret);
    return darwin_to_libusb (ret);
  }

  /* catch buggy hubs (which appear to be virtual). Apple's own USB prober has problems with these devices. */
  if (libusb_le16_to_cpu (dev->dev_descriptor.idProduct) != idProduct) {
    /* not a valid device */
    usbi_warn (NULL, "idProduct from iokit (%04x) does not match idProduct in descriptor (%04x). skipping device",
               idProduct, libusb_le16_to_cpu (dev->dev_descriptor.idProduct));
    return LIBUSB_ERROR_NO_DEVICE;
  }

  usbi_dbg ("cached device descriptor:");
  usbi_dbg ("  bDescriptorType:    0x%02x", dev->dev_descriptor.bDescriptorType);
  usbi_dbg ("  bcdUSB:             0x%04x", libusb_le16_to_cpu (dev->dev_descriptor.bcdUSB));
  usbi_dbg ("  bDeviceClass:       0x%02x", dev->dev_descriptor.bDeviceClass);
  usbi_dbg ("  bDeviceSubClass:    0x%02x", dev->dev_descriptor.bDeviceSubClass);
  usbi_dbg ("  bDeviceProtocol:    0x%02x", dev->dev_descriptor.bDeviceProtocol);
  usbi_dbg ("  bMaxPacketSize0:    0x%02x", dev->dev_descriptor.bMaxPacketSize0);
  usbi_dbg ("  idVendor:           0x%04x", libusb_le16_to_cpu (dev->dev_descriptor.idVendor));
  usbi_dbg ("  idProduct:          0x%04x", libusb_le16_to_cpu (dev->dev_descriptor.idProduct));
  usbi_dbg ("  bcdDevice:          0x%04x", libusb_le16_to_cpu (dev->dev_descriptor.bcdDevice));
  usbi_dbg ("  iManufacturer:      0x%02x", dev->dev_descriptor.iManufacturer);
  usbi_dbg ("  iProduct:           0x%02x", dev->dev_descriptor.iProduct);
  usbi_dbg ("  iSerialNumber:      0x%02x", dev->dev_descriptor.iSerialNumber);
  usbi_dbg ("  bNumConfigurations: 0x%02x", dev->dev_descriptor.bNumConfigurations);

  dev->can_enumerate = 1;

  return LIBUSB_SUCCESS;
}

/* Returns 1 on success, 0 on failure. */
static bool get_device_port (io_service_t service, UInt8 *port) {
  IOReturn kresult;
  io_service_t parent;
  bool ret = false;

  if (get_ioregistry_value_number (service, CFSTR("PortNum"), kCFNumberSInt8Type, port)) {
    return true;
  }

  kresult = IORegistryEntryGetParentEntry (service, kIOServicePlane, &parent);
  if (kIOReturnSuccess == kresult) {
    ret = get_ioregistry_value_data (parent, CFSTR("port"), 1, port);
    IOObjectRelease (parent);
  }

  return ret;
}

/* Returns 1 on success, 0 on failure. */
static bool get_device_parent_sessionID(io_service_t service, UInt64 *parent_sessionID) {
  IOReturn kresult;
  io_service_t parent;

  /* Walk up the tree in the IOService plane until we find a parent that has a sessionID */
  parent = service;
  while((kresult = IORegistryEntryGetParentEntry (parent, kIOUSBPlane, &parent)) == kIOReturnSuccess) {
    if (get_ioregistry_value_number (parent, CFSTR("sessionID"), kCFNumberSInt64Type, parent_sessionID)) {
        /* Success */
        return true;
    }
  }

  /* We ran out of parents */
  return false;
}

static enum libusb_error darwin_get_cached_device(io_service_t service, struct darwin_cached_device **cached_out,
                                                  UInt64 *old_session_id) {
  struct darwin_cached_device *new_device;
  UInt64 sessionID = 0, parent_sessionID = 0;
  UInt32 locationID = 0;
  enum libusb_error ret = LIBUSB_SUCCESS;
  usb_device_t **device;
  UInt8 port = 0;

  /* assuming sessionID != 0 normally (never seen it be 0) */
  *old_session_id = 0;
  *cached_out = NULL;

  /* get some info from the io registry */
  (void) get_ioregistry_value_number (service, CFSTR("sessionID"), kCFNumberSInt64Type, &sessionID);
  (void) get_ioregistry_value_number (service, CFSTR("locationID"), kCFNumberSInt32Type, &locationID);
  if (!get_device_port (service, &port)) {
    usbi_dbg("could not get connected port number");
  }

  usbi_dbg("finding cached device for sessionID 0x%" PRIx64, sessionID);

  if (get_device_parent_sessionID(service, &parent_sessionID)) {
    usbi_dbg("parent sessionID: 0x%" PRIx64, parent_sessionID);
  }

  usbi_mutex_lock(&darwin_cached_devices_lock);
  do {
    list_for_each_entry(new_device, &darwin_cached_devices, list, struct darwin_cached_device) {
      usbi_dbg("matching sessionID/locationID 0x%" PRIx64 "/0x%x against cached device with sessionID/locationID 0x%" PRIx64 "/0x%x",
               sessionID, locationID, new_device->session, new_device->location);
      if (new_device->location == locationID && new_device->in_reenumerate) {
        usbi_dbg ("found cached device with matching location that is being re-enumerated");
        *old_session_id = new_device->session;
        break;
      }

      if (new_device->session == sessionID) {
        usbi_dbg("using cached device for device");
        *cached_out = new_device;
        break;
      }
    }

    if (*cached_out)
      break;

    usbi_dbg("caching new device with sessionID 0x%" PRIx64, sessionID);

    device = darwin_device_from_service (service);
    if (!device) {
      ret = LIBUSB_ERROR_NO_DEVICE;
      break;
    }

    if (!(*old_session_id)) {
      new_device = calloc (1, sizeof (*new_device));
      if (!new_device) {
        ret = LIBUSB_ERROR_NO_MEM;
        break;
      }

      /* add this device to the cached device list */
      list_add(&new_device->list, &darwin_cached_devices);

      (*device)->GetDeviceAddress (device, (USBDeviceAddress *)&new_device->address);

      /* keep a reference to this device */
      darwin_ref_cached_device(new_device);

      (*device)->GetLocationID (device, &new_device->location);
      new_device->port = port;
      new_device->parent_session = parent_sessionID;
    }

    /* keep track of devices regardless of if we successfully enumerate them to
       prevent them from being enumerated multiple times */
    *cached_out = new_device;

    new_device->session = sessionID;
    new_device->device = device;

    /* cache the device descriptor */
    ret = darwin_cache_device_descriptor(new_device);
    if (ret)
      break;

    if (new_device->can_enumerate) {
      snprintf(new_device->sys_path, 20, "%03i-%04x-%04x-%02x-%02x", new_device->address,
               libusb_le16_to_cpu (new_device->dev_descriptor.idVendor),
               libusb_le16_to_cpu (new_device->dev_descriptor.idProduct),
               new_device->dev_descriptor.bDeviceClass, new_device->dev_descriptor.bDeviceSubClass);
    }
  } while (0);

  usbi_mutex_unlock(&darwin_cached_devices_lock);

  return ret;
}

static enum libusb_error process_new_device (struct libusb_context *ctx, struct darwin_cached_device *cached_device,
                                             UInt64 old_session_id) {
  struct darwin_device_priv *priv;
  struct libusb_device *dev = NULL;
  UInt8 devSpeed;
  enum libusb_error ret = LIBUSB_SUCCESS;

  do {
    /* check current active configuration (and cache the first configuration value--
       which may be used by claim_interface) */
    ret = darwin_check_configuration (ctx, cached_device);
    if (ret)
      break;

    if (0 != old_session_id) {
      usbi_dbg ("re-using existing device from context %p for with session 0x%" PRIx64 " new session 0x%" PRIx64,
                ctx, old_session_id, cached_device->session);
      /* save the libusb device before the session id is updated */
      dev = usbi_get_device_by_session_id (ctx, (unsigned long) old_session_id);
    }

    if (!dev) {
      usbi_dbg ("allocating new device in context %p for with session 0x%" PRIx64,
                ctx, cached_device->session);

      dev = usbi_alloc_device(ctx, (unsigned long) cached_device->session);
      if (!dev) {
        return LIBUSB_ERROR_NO_MEM;
      }

      priv = usbi_get_device_priv(dev);

      priv->dev = cached_device;
      darwin_ref_cached_device (priv->dev);
      dev->port_number    = cached_device->port;
      dev->bus_number     = cached_device->location >> 24;
      assert(cached_device->address <= UINT8_MAX);
      dev->device_address = (uint8_t)cached_device->address;
    } else {
      priv = usbi_get_device_priv(dev);
    }

    static_assert(sizeof(dev->device_descriptor) == sizeof(cached_device->dev_descriptor),
                  "mismatch between libusb and IOKit device descriptor sizes");
    memcpy(&dev->device_descriptor, &cached_device->dev_descriptor, LIBUSB_DT_DEVICE_SIZE);
    usbi_localize_device_descriptor(&dev->device_descriptor);
    dev->session_data = cached_device->session;

    if (cached_device->parent_session > 0) {
      dev->parent_dev = usbi_get_device_by_session_id (ctx, (unsigned long) cached_device->parent_session);
    } else {
      dev->parent_dev = NULL;
    }

    (*(priv->dev->device))->GetDeviceSpeed (priv->dev->device, &devSpeed);

    switch (devSpeed) {
    case kUSBDeviceSpeedLow: dev->speed = LIBUSB_SPEED_LOW; break;
    case kUSBDeviceSpeedFull: dev->speed = LIBUSB_SPEED_FULL; break;
    case kUSBDeviceSpeedHigh: dev->speed = LIBUSB_SPEED_HIGH; break;
#if MAC_OS_X_VERSION_MAX_ALLOWED >= 1070
    case kUSBDeviceSpeedSuper: dev->speed = LIBUSB_SPEED_SUPER; break;
#endif
#if MAC_OS_X_VERSION_MAX_ALLOWED >= 101200
    case kUSBDeviceSpeedSuperPlus: dev->speed = LIBUSB_SPEED_SUPER_PLUS; break;
#endif
    default:
      usbi_warn (ctx, "Got unknown device speed %d", devSpeed);
    }

    ret = usbi_sanitize_device (dev);
    if (ret < 0)
      break;

    usbi_dbg ("found device with address %d port = %d parent = %p at %p", dev->device_address,
              dev->port_number, (void *) dev->parent_dev, priv->dev->sys_path);

  } while (0);

  if (!cached_device->in_reenumerate && 0 == ret) {
    usbi_connect_device (dev);
  } else {
    libusb_unref_device (dev);
  }

  return ret;
}

static enum libusb_error darwin_scan_devices(struct libusb_context *ctx) {
  struct darwin_cached_device *cached_device;
  UInt64 old_session_id;
  io_iterator_t deviceIterator;
  io_service_t service;
  IOReturn kresult;
  int ret;

  kresult = usb_setup_device_iterator (&deviceIterator, 0);
  if (kresult != kIOReturnSuccess)
    return darwin_to_libusb (kresult);

  while ((service = IOIteratorNext (deviceIterator))) {
    ret = darwin_get_cached_device (service, &cached_device, &old_session_id);
    if (ret < 0 || !cached_device->can_enumerate) {
      continue;
    }

    (void) process_new_device (ctx, cached_device, old_session_id);

    IOObjectRelease(service);
  }

  IOObjectRelease(deviceIterator);

  return LIBUSB_SUCCESS;
}

static int darwin_open (struct libusb_device_handle *dev_handle) {
  struct darwin_device_handle_priv *priv = usbi_get_device_handle_priv(dev_handle);
  struct darwin_cached_device *dpriv = DARWIN_CACHED_DEVICE(dev_handle->dev);
  IOReturn kresult;

  if (0 == dpriv->open_count) {
    /* try to open the device */
    kresult = (*(dpriv->device))->USBDeviceOpenSeize (dpriv->device);
    if (kresult != kIOReturnSuccess) {
      usbi_warn (HANDLE_CTX (dev_handle), "USBDeviceOpen: %s", darwin_error_str(kresult));

      if (kIOReturnExclusiveAccess != kresult) {
        return darwin_to_libusb (kresult);
      }

      /* it is possible to perform some actions on a device that is not open so do not return an error */
      priv->is_open = false;
    } else {
      priv->is_open = true;
    }

    /* create async event source */
    kresult = (*(dpriv->device))->CreateDeviceAsyncEventSource (dpriv->device, &priv->cfSource);
    if (kresult != kIOReturnSuccess) {
      usbi_err (HANDLE_CTX (dev_handle), "CreateDeviceAsyncEventSource: %s", darwin_error_str(kresult));

      if (priv->is_open) {
        (*(dpriv->device))->USBDeviceClose (dpriv->device);
      }

      priv->is_open = false;

      return darwin_to_libusb (kresult);
    }

    CFRetain (libusb_darwin_acfl);

    /* add the cfSource to the aync run loop */
    CFRunLoopAddSource(libusb_darwin_acfl, priv->cfSource, kCFRunLoopCommonModes);
  }

  /* device opened successfully */
  dpriv->open_count++;

  usbi_dbg ("device open for access");

  return 0;
}

static void darwin_close (struct libusb_device_handle *dev_handle) {
  struct darwin_device_handle_priv *priv = usbi_get_device_handle_priv(dev_handle);
  struct darwin_cached_device *dpriv = DARWIN_CACHED_DEVICE(dev_handle->dev);
  IOReturn kresult;
  int i;

  if (dpriv->open_count == 0) {
    /* something is probably very wrong if this is the case */
    usbi_err (HANDLE_CTX (dev_handle), "Close called on a device that was not open!");
    return;
  }

  dpriv->open_count--;

  /* make sure all interfaces are released */
  for (i = 0 ; i < USB_MAXINTERFACES ; i++)
    if (dev_handle->claimed_interfaces & (1U << i))
      libusb_release_interface (dev_handle, i);

  if (0 == dpriv->open_count) {
    /* delete the device's async event source */
    if (priv->cfSource) {
      CFRunLoopRemoveSource (libusb_darwin_acfl, priv->cfSource, kCFRunLoopDefaultMode);
      CFRelease (priv->cfSource);
      priv->cfSource = NULL;
      CFRelease (libusb_darwin_acfl);
    }

    if (priv->is_open) {
      /* close the device */
      kresult = (*(dpriv->device))->USBDeviceClose(dpriv->device);
      if (kresult != kIOReturnSuccess) {
        /* Log the fact that we had a problem closing the file, however failing a
         * close isn't really an error, so return success anyway */
        usbi_warn (HANDLE_CTX (dev_handle), "USBDeviceClose: %s", darwin_error_str(kresult));
      }
    }
  }
}

static int darwin_get_configuration(struct libusb_device_handle *dev_handle, uint8_t *config) {
  struct darwin_cached_device *dpriv = DARWIN_CACHED_DEVICE(dev_handle->dev);

  *config = dpriv->active_config;

  return LIBUSB_SUCCESS;
}

static enum libusb_error darwin_set_configuration(struct libusb_device_handle *dev_handle, int config) {
  struct darwin_cached_device *dpriv = DARWIN_CACHED_DEVICE(dev_handle->dev);
  IOReturn kresult;
  uint8_t i;

  if (config == -1)
    config = 0;

  /* Setting configuration will invalidate the interface, so we need
     to reclaim it. First, dispose of existing interfaces, if any. */
  for (i = 0 ; i < USB_MAXINTERFACES ; i++)
    if (dev_handle->claimed_interfaces & (1U << i))
      darwin_release_interface (dev_handle, i);

  kresult = (*(dpriv->device))->SetConfiguration (dpriv->device, (UInt8)config);
  if (kresult != kIOReturnSuccess)
    return darwin_to_libusb (kresult);

  /* Reclaim any interfaces. */
  for (i = 0 ; i < USB_MAXINTERFACES ; i++)
    if (dev_handle->claimed_interfaces & (1U << i))
      darwin_claim_interface (dev_handle, i);

  dpriv->active_config = (UInt8)config;

  return LIBUSB_SUCCESS;
}

static IOReturn darwin_get_interface (usb_device_t **darwin_device, uint8_t ifc, io_service_t *usbInterfacep) {
  IOUSBFindInterfaceRequest request;
  IOReturn                  kresult;
  io_iterator_t             interface_iterator;
  UInt8                     bInterfaceNumber;
  bool                      ret;

  *usbInterfacep = IO_OBJECT_NULL;

  /* Setup the Interface Request */
  request.bInterfaceClass    = kIOUSBFindInterfaceDontCare;
  request.bInterfaceSubClass = kIOUSBFindInterfaceDontCare;
  request.bInterfaceProtocol = kIOUSBFindInterfaceDontCare;
  request.bAlternateSetting  = kIOUSBFindInterfaceDontCare;

  kresult = (*(darwin_device))->CreateInterfaceIterator(darwin_device, &request, &interface_iterator);
  if (kresult != kIOReturnSuccess)
    return kresult;

  while ((*usbInterfacep = IOIteratorNext(interface_iterator))) {
    /* find the interface number */
    ret = get_ioregistry_value_number (*usbInterfacep, CFSTR("bInterfaceNumber"), kCFNumberSInt8Type,
                                       &bInterfaceNumber);

    if (ret && bInterfaceNumber == ifc) {
      break;
    }

    (void) IOObjectRelease (*usbInterfacep);
  }

  /* done with the interface iterator */
  IOObjectRelease(interface_iterator);

  return kIOReturnSuccess;
}

static enum libusb_error get_endpoints (struct libusb_device_handle *dev_handle, uint8_t iface) {
  struct darwin_device_handle_priv *priv = usbi_get_device_handle_priv(dev_handle);

  /* current interface */
  struct darwin_interface *cInterface = &priv->interfaces[iface];

  IOReturn kresult;

  UInt8 numep, direction, number;
  UInt8 dont_care1, dont_care3;
  UInt16 dont_care2;
  int rc;

  usbi_dbg ("building table of endpoints.");

  /* retrieve the total number of endpoints on this interface */
  kresult = (*(cInterface->interface))->GetNumEndpoints(cInterface->interface, &numep);
  if (kresult != kIOReturnSuccess) {
    usbi_err (HANDLE_CTX (dev_handle), "can't get number of endpoints for interface: %s", darwin_error_str(kresult));
    return darwin_to_libusb (kresult);
  }

  /* iterate through pipe references */
  for (UInt8 i = 1 ; i <= numep ; i++) {
    kresult = (*(cInterface->interface))->GetPipeProperties(cInterface->interface, i, &direction, &number, &dont_care1,
                                                            &dont_care2, &dont_care3);

    if (kresult != kIOReturnSuccess) {
      /* probably a buggy device. try to get the endpoint address from the descriptors */
      struct libusb_config_descriptor *config;
      const struct libusb_endpoint_descriptor *endpoint_desc;
      UInt8 alt_setting;

      kresult = (*(cInterface->interface))->GetAlternateSetting (cInterface->interface, &alt_setting);
      if (kresult != kIOReturnSuccess) {
        usbi_err (HANDLE_CTX (dev_handle), "can't get alternate setting for interface");
        return darwin_to_libusb (kresult);
      }

      rc = libusb_get_active_config_descriptor (dev_handle->dev, &config);
      if (LIBUSB_SUCCESS != rc) {
        return rc;
      }

      endpoint_desc = config->interface[iface].altsetting[alt_setting].endpoint + i - 1;

      cInterface->endpoint_addrs[i - 1] = endpoint_desc->bEndpointAddress;
    } else {
      cInterface->endpoint_addrs[i - 1] = (UInt8)(((kUSBIn == direction) << kUSBRqDirnShift) | (number & LIBUSB_ENDPOINT_ADDRESS_MASK));
    }

    usbi_dbg ("interface: %i pipe %i: dir: %i number: %i", iface, i, cInterface->endpoint_addrs[i - 1] >> kUSBRqDirnShift,
              cInterface->endpoint_addrs[i - 1] & LIBUSB_ENDPOINT_ADDRESS_MASK);
  }

  cInterface->num_endpoints = numep;

  return LIBUSB_SUCCESS;
}

static int darwin_claim_interface(struct libusb_device_handle *dev_handle, uint8_t iface) {
  struct darwin_cached_device *dpriv = DARWIN_CACHED_DEVICE(dev_handle->dev);
  struct darwin_device_handle_priv *priv = usbi_get_device_handle_priv(dev_handle);
  io_service_t          usbInterface = IO_OBJECT_NULL;
  IOReturn              kresult;
  enum libusb_error     ret;
  IOCFPlugInInterface **plugInInterface = NULL;
  SInt32                score;

  /* current interface */
  struct darwin_interface *cInterface = &priv->interfaces[iface];

  kresult = darwin_get_interface (dpriv->device, iface, &usbInterface);
  if (kresult != kIOReturnSuccess)
    return darwin_to_libusb (kresult);

  /* make sure we have an interface */
  if (!usbInterface && dpriv->first_config != 0) {
    usbi_info (HANDLE_CTX (dev_handle), "no interface found; setting configuration: %d", dpriv->first_config);

    /* set the configuration */
    ret = darwin_set_configuration (dev_handle, (int) dpriv->first_config);
    if (ret != LIBUSB_SUCCESS) {
      usbi_err (HANDLE_CTX (dev_handle), "could not set configuration");
      return ret;
    }

    kresult = darwin_get_interface (dpriv->device, iface, &usbInterface);
    if (kresult != kIOReturnSuccess) {
      usbi_err (HANDLE_CTX (dev_handle), "darwin_get_interface: %s", darwin_error_str(kresult));
      return darwin_to_libusb (kresult);
    }
  }

  if (!usbInterface) {
    usbi_err (HANDLE_CTX (dev_handle), "interface not found");
    return LIBUSB_ERROR_NOT_FOUND;
  }

  /* get an interface to the device's interface */
  kresult = IOCreatePlugInInterfaceForService (usbInterface, kIOUSBInterfaceUserClientTypeID,
                                               kIOCFPlugInInterfaceID, &plugInInterface, &score);

  /* ignore release error */
  (void)IOObjectRelease (usbInterface);

  if (kresult != kIOReturnSuccess) {
    usbi_err (HANDLE_CTX (dev_handle), "IOCreatePlugInInterfaceForService: %s", darwin_error_str(kresult));
    return darwin_to_libusb (kresult);
  }

  if (!plugInInterface) {
    usbi_err (HANDLE_CTX (dev_handle), "plugin interface not found");
    return LIBUSB_ERROR_NOT_FOUND;
  }

  /* Do the actual claim */
  kresult = (*plugInInterface)->QueryInterface(plugInInterface,
                                               CFUUIDGetUUIDBytes(InterfaceInterfaceID),
                                               (LPVOID)&cInterface->interface);
  /* We no longer need the intermediate plug-in */
  /* Use release instead of IODestroyPlugInInterface to avoid stopping IOServices associated with this device */
  (*plugInInterface)->Release (plugInInterface);
  if (kresult != kIOReturnSuccess || !cInterface->interface) {
    usbi_err (HANDLE_CTX (dev_handle), "QueryInterface: %s", darwin_error_str(kresult));
    return darwin_to_libusb (kresult);
  }

  /* claim the interface */
  kresult = (*(cInterface->interface))->USBInterfaceOpen(cInterface->interface);
  if (kresult != kIOReturnSuccess) {
    usbi_err (HANDLE_CTX (dev_handle), "USBInterfaceOpen: %s", darwin_error_str(kresult));
    return darwin_to_libusb (kresult);
  }

  /* update list of endpoints */
  ret = get_endpoints (dev_handle, iface);
  if (ret) {
    /* this should not happen */
    darwin_release_interface (dev_handle, iface);
    usbi_err (HANDLE_CTX (dev_handle), "could not build endpoint table");
    return ret;
  }

  cInterface->cfSource = NULL;

  /* create async event source */
  kresult = (*(cInterface->interface))->CreateInterfaceAsyncEventSource (cInterface->interface, &cInterface->cfSource);
  if (kresult != kIOReturnSuccess) {
    usbi_err (HANDLE_CTX (dev_handle), "could not create async event source");

    /* can't continue without an async event source */
    (void)darwin_release_interface (dev_handle, iface);

    return darwin_to_libusb (kresult);
  }

  /* add the cfSource to the async thread's run loop */
  CFRunLoopAddSource(libusb_darwin_acfl, cInterface->cfSource, kCFRunLoopDefaultMode);

  usbi_dbg ("interface opened");

  return LIBUSB_SUCCESS;
}

static int darwin_release_interface(struct libusb_device_handle *dev_handle, uint8_t iface) {
  struct darwin_device_handle_priv *priv = usbi_get_device_handle_priv(dev_handle);
  IOReturn kresult;

  /* current interface */
  struct darwin_interface *cInterface = &priv->interfaces[iface];

  /* Check to see if an interface is open */
  if (!cInterface->interface)
    return LIBUSB_SUCCESS;

  /* clean up endpoint data */
  cInterface->num_endpoints = 0;

  /* delete the interface's async event source */
  if (cInterface->cfSource) {
    CFRunLoopRemoveSource (libusb_darwin_acfl, cInterface->cfSource, kCFRunLoopDefaultMode);
    CFRelease (cInterface->cfSource);
  }

  kresult = (*(cInterface->interface))->USBInterfaceClose(cInterface->interface);
  if (kresult != kIOReturnSuccess)
    usbi_warn (HANDLE_CTX (dev_handle), "USBInterfaceClose: %s", darwin_error_str(kresult));

  kresult = (*(cInterface->interface))->Release(cInterface->interface);
  if (kresult != kIOReturnSuccess)
    usbi_warn (HANDLE_CTX (dev_handle), "Release: %s", darwin_error_str(kresult));

  cInterface->interface = (usb_interface_t **) IO_OBJECT_NULL;

  return darwin_to_libusb (kresult);
}

static int darwin_set_interface_altsetting(struct libusb_device_handle *dev_handle, uint8_t iface, uint8_t altsetting) {
  struct darwin_device_handle_priv *priv = usbi_get_device_handle_priv(dev_handle);
  IOReturn kresult;
  enum libusb_error ret;

  /* current interface */
  struct darwin_interface *cInterface = &priv->interfaces[iface];

  if (!cInterface->interface)
    return LIBUSB_ERROR_NO_DEVICE;

  kresult = (*(cInterface->interface))->SetAlternateInterface (cInterface->interface, altsetting);
  if (kresult != kIOReturnSuccess)
    darwin_reset_device (dev_handle);

  /* update list of endpoints */
  ret = get_endpoints (dev_handle, iface);
  if (ret) {
    /* this should not happen */
    darwin_release_interface (dev_handle, iface);
    usbi_err (HANDLE_CTX (dev_handle), "could not build endpoint table");
    return ret;
  }

  return darwin_to_libusb (kresult);
}

static int darwin_clear_halt(struct libusb_device_handle *dev_handle, unsigned char endpoint) {
  /* current interface */
  struct darwin_interface *cInterface;
  IOReturn kresult;
  uint8_t pipeRef;

  /* determine the interface/endpoint to use */
  if (ep_to_pipeRef (dev_handle, endpoint, &pipeRef, NULL, &cInterface) != 0) {
    usbi_err (HANDLE_CTX (dev_handle), "endpoint not found on any open interface");

    return LIBUSB_ERROR_NOT_FOUND;
  }

  /* newer versions of darwin support clearing additional bits on the device's endpoint */
  kresult = (*(cInterface->interface))->ClearPipeStallBothEnds(cInterface->interface, pipeRef);
  if (kresult != kIOReturnSuccess)
    usbi_warn (HANDLE_CTX (dev_handle), "ClearPipeStall: %s", darwin_error_str (kresult));

  return darwin_to_libusb (kresult);
}

static int darwin_restore_state (struct libusb_device_handle *dev_handle, int8_t active_config,
                                 unsigned long claimed_interfaces) {
  struct darwin_cached_device *dpriv = DARWIN_CACHED_DEVICE(dev_handle->dev);
  struct darwin_device_handle_priv *priv = usbi_get_device_handle_priv(dev_handle);
  int open_count = dpriv->open_count;
  int ret;

  /* clear claimed interfaces temporarily */
  dev_handle->claimed_interfaces = 0;

  /* close and re-open the device */
  priv->is_open = false;
  dpriv->open_count = 1;

  /* clean up open interfaces */
  (void) darwin_close (dev_handle);

  /* re-open the device */
  ret = darwin_open (dev_handle);
  dpriv->open_count = open_count;
  if (LIBUSB_SUCCESS != ret) {
    /* could not restore configuration */
    return LIBUSB_ERROR_NOT_FOUND;
  }

  if (dpriv->active_config != active_config) {
    usbi_dbg ("darwin/restore_state: restoring configuration %d...", active_config);

    ret = darwin_set_configuration (dev_handle, active_config);
    if (LIBUSB_SUCCESS != ret) {
      usbi_dbg ("darwin/restore_state: could not restore configuration");
      return LIBUSB_ERROR_NOT_FOUND;
    }
  }

  usbi_dbg ("darwin/restore_state: reclaiming interfaces");

  if (claimed_interfaces) {
    for (uint8_t iface = 0 ; iface < USB_MAXINTERFACES ; ++iface) {
      if (!(claimed_interfaces & (1U << iface))) {
        continue;
      }

      usbi_dbg ("darwin/restore_state: re-claiming interface %u", iface);

      ret = darwin_claim_interface (dev_handle, iface);
      if (LIBUSB_SUCCESS != ret) {
        usbi_dbg ("darwin/restore_state: could not claim interface %u", iface);
        return LIBUSB_ERROR_NOT_FOUND;
      }

      dev_handle->claimed_interfaces |= 1U << iface;
    }
  }

  usbi_dbg ("darwin/restore_state: device state restored");

  return LIBUSB_SUCCESS;
}

static int darwin_reset_device(struct libusb_device_handle *dev_handle) {
  struct darwin_cached_device *dpriv = DARWIN_CACHED_DEVICE(dev_handle->dev);
  unsigned long claimed_interfaces = dev_handle->claimed_interfaces;
  int8_t active_config = dpriv->active_config;
  IOUSBDeviceDescriptor descriptor;
  IOUSBConfigurationDescriptorPtr cached_configuration;
  IOUSBConfigurationDescriptor *cached_configurations;
  IOReturn kresult;
  UInt8 i;

  if (dpriv->in_reenumerate) {
    /* ack, two (or more) threads are trying to reset the device! abort! */
    return LIBUSB_ERROR_NOT_FOUND;
  }

  dpriv->in_reenumerate = true;

  /* store copies of descriptors so they can be compared after the reset */
  memcpy (&descriptor, &dpriv->dev_descriptor, sizeof (descriptor));
  cached_configurations = alloca (sizeof (*cached_configurations) * descriptor.bNumConfigurations);

  for (i = 0 ; i < descriptor.bNumConfigurations ; ++i) {
    (*(dpriv->device))->GetConfigurationDescriptorPtr (dpriv->device, i, &cached_configuration);
    memcpy (cached_configurations + i, cached_configuration, sizeof (cached_configurations[i]));
  }

  /* from macOS 10.11 ResetDevice no longer does anything so just use USBDeviceReEnumerate */
  kresult = (*(dpriv->device))->USBDeviceReEnumerate (dpriv->device, 0);
  if (kresult != kIOReturnSuccess) {
    usbi_err (HANDLE_CTX (dev_handle), "USBDeviceReEnumerate: %s", darwin_error_str (kresult));
    dpriv->in_reenumerate = false;
    return darwin_to_libusb (kresult);
  }

  usbi_dbg ("darwin/reset_device: waiting for re-enumeration to complete...");

  while (dpriv->in_reenumerate) {
    struct timespec delay = {.tv_sec = 0, .tv_nsec = 1000};
    nanosleep (&delay, NULL);
  }

  /* compare descriptors */
  usbi_dbg ("darwin/reset_device: checking whether descriptors changed");

  if (memcmp (&descriptor, &dpriv->dev_descriptor, sizeof (descriptor))) {
    /* device descriptor changed. need to return not found. */
    usbi_dbg ("darwin/reset_device: device descriptor changed");
    return LIBUSB_ERROR_NOT_FOUND;
  }

  for (i = 0 ; i < descriptor.bNumConfigurations ; ++i) {
    (void) (*(dpriv->device))->GetConfigurationDescriptorPtr (dpriv->device, i, &cached_configuration);
    if (memcmp (cached_configuration, cached_configurations + i, sizeof (cached_configurations[i]))) {
      usbi_dbg ("darwin/reset_device: configuration descriptor %d changed", i);
      return LIBUSB_ERROR_NOT_FOUND;
    }
  }

  usbi_dbg ("darwin/reset_device: device reset complete. restoring state...");

  return darwin_restore_state (dev_handle, active_config, claimed_interfaces);
}

static int darwin_kernel_driver_active(struct libusb_device_handle *dev_handle, uint8_t interface) {
  struct darwin_cached_device *dpriv = DARWIN_CACHED_DEVICE(dev_handle->dev);
  io_service_t usbInterface;
  CFTypeRef driver;
  IOReturn kresult;

  kresult = darwin_get_interface (dpriv->device, interface, &usbInterface);
  if (kresult != kIOReturnSuccess) {
    usbi_err (HANDLE_CTX (dev_handle), "darwin_get_interface: %s", darwin_error_str(kresult));

    return darwin_to_libusb (kresult);
  }

  driver = IORegistryEntryCreateCFProperty (usbInterface, kIOBundleIdentifierKey, kCFAllocatorDefault, 0);
  IOObjectRelease (usbInterface);

  if (driver) {
    CFRelease (driver);

    return 1;
  }

  /* no driver */
  return 0;
}

static void darwin_destroy_device(struct libusb_device *dev) {
  struct darwin_device_priv *dpriv = usbi_get_device_priv(dev);

  if (dpriv->dev) {
    /* need to hold the lock in case this is the last reference to the device */
    usbi_mutex_lock(&darwin_cached_devices_lock);
    darwin_deref_cached_device (dpriv->dev);
    dpriv->dev = NULL;
    usbi_mutex_unlock(&darwin_cached_devices_lock);
  }
}

static int submit_bulk_transfer(struct usbi_transfer *itransfer) {
  struct libusb_transfer *transfer = USBI_TRANSFER_TO_LIBUSB_TRANSFER(itransfer);

  IOReturn               ret;
  uint8_t                transferType;
  uint8_t                pipeRef;
  uint16_t               maxPacketSize;

  struct darwin_interface *cInterface;
#if InterfaceVersion >= 550
  IOUSBEndpointProperties pipeProperties = {.bVersion = kUSBEndpointPropertiesVersion3};
#else
  /* None of the values below are used in libusb for bulk transfers */
  uint8_t                 direction, number, interval;
#endif

  if (ep_to_pipeRef (transfer->dev_handle, transfer->endpoint, &pipeRef, NULL, &cInterface) != 0) {
    usbi_err (TRANSFER_CTX (transfer), "endpoint not found on any open interface");

    return LIBUSB_ERROR_NOT_FOUND;
  }

#if InterfaceVersion >= 550
  ret = (*(cInterface->interface))->GetPipePropertiesV3 (cInterface->interface, pipeRef, &pipeProperties);

  transferType = pipeProperties.bTransferType;
  maxPacketSize = pipeProperties.wMaxPacketSize;
#else
  ret = (*(cInterface->interface))->GetPipeProperties (cInterface->interface, pipeRef, &direction, &number,
                                                       &transferType, &maxPacketSize, &interval);
#endif

  if (ret) {
    usbi_err (TRANSFER_CTX (transfer), "bulk transfer failed (dir = %s): %s (code = 0x%08x)", IS_XFERIN(transfer) ? "In" : "Out",
              darwin_error_str(ret), ret);
    return darwin_to_libusb (ret);
  }

  if (0 != (transfer->length % maxPacketSize)) {
    /* do not need a zero packet */
    transfer->flags &= ~LIBUSB_TRANSFER_ADD_ZERO_PACKET;
  }

  /* submit the request */
  /* timeouts are unavailable on interrupt endpoints */
  if (transferType == kUSBInterrupt) {
    if (IS_XFERIN(transfer))
      ret = (*(cInterface->interface))->ReadPipeAsync(cInterface->interface, pipeRef, transfer->buffer,
                                                      (UInt32)transfer->length, darwin_async_io_callback, itransfer);
    else
      ret = (*(cInterface->interface))->WritePipeAsync(cInterface->interface, pipeRef, transfer->buffer,
                                                       (UInt32)transfer->length, darwin_async_io_callback, itransfer);
  } else {
    itransfer->timeout_flags |= USBI_TRANSFER_OS_HANDLES_TIMEOUT;

    if (IS_XFERIN(transfer))
      ret = (*(cInterface->interface))->ReadPipeAsyncTO(cInterface->interface, pipeRef, transfer->buffer,
                                                        (UInt32)transfer->length, transfer->timeout, transfer->timeout,
                                                        darwin_async_io_callback, itransfer);
    else
      ret = (*(cInterface->interface))->WritePipeAsyncTO(cInterface->interface, pipeRef, transfer->buffer,
                                                         (UInt32)transfer->length, transfer->timeout, transfer->timeout,
                                                         darwin_async_io_callback, itransfer);
  }

  if (ret)
    usbi_err (TRANSFER_CTX (transfer), "bulk transfer failed (dir = %s): %s (code = 0x%08x)", IS_XFERIN(transfer) ? "In" : "Out",
               darwin_error_str(ret), ret);

  return darwin_to_libusb (ret);
}

#if InterfaceVersion >= 550
static int submit_stream_transfer(struct usbi_transfer *itransfer) {
  struct libusb_transfer *transfer = USBI_TRANSFER_TO_LIBUSB_TRANSFER(itransfer);
  struct darwin_interface *cInterface;
  uint8_t pipeRef;
  IOReturn ret;

  if (ep_to_pipeRef (transfer->dev_handle, transfer->endpoint, &pipeRef, NULL, &cInterface) != 0) {
    usbi_err (TRANSFER_CTX (transfer), "endpoint not found on any open interface");

    return LIBUSB_ERROR_NOT_FOUND;
  }

  itransfer->timeout_flags |= USBI_TRANSFER_OS_HANDLES_TIMEOUT;

  if (IS_XFERIN(transfer))
    ret = (*(cInterface->interface))->ReadStreamsPipeAsyncTO(cInterface->interface, pipeRef, itransfer->stream_id,
                                                             transfer->buffer, (UInt32)transfer->length, transfer->timeout,
                                                             transfer->timeout, darwin_async_io_callback, itransfer);
  else
    ret = (*(cInterface->interface))->WriteStreamsPipeAsyncTO(cInterface->interface, pipeRef, itransfer->stream_id,
                                                              transfer->buffer, (UInt32)transfer->length, transfer->timeout,
                                                              transfer->timeout, darwin_async_io_callback, itransfer);

  if (ret)
    usbi_err (TRANSFER_CTX (transfer), "bulk stream transfer failed (dir = %s): %s (code = 0x%08x)", IS_XFERIN(transfer) ? "In" : "Out",
               darwin_error_str(ret), ret);

  return darwin_to_libusb (ret);
}
#endif

static int submit_iso_transfer(struct usbi_transfer *itransfer) {
  struct libusb_transfer *transfer = USBI_TRANSFER_TO_LIBUSB_TRANSFER(itransfer);
  struct darwin_transfer_priv *tpriv = usbi_get_transfer_priv(itransfer);

  IOReturn kresult;
  uint8_t direction, number, interval, pipeRef, transferType;
  uint16_t maxPacketSize;
  UInt64 frame;
  AbsoluteTime atTime;
  int i;

  struct darwin_interface *cInterface;

  /* construct an array of IOUSBIsocFrames, reuse the old one if the sizes are the same */
  if (tpriv->num_iso_packets != transfer->num_iso_packets) {
    free(tpriv->isoc_framelist);
    tpriv->isoc_framelist = NULL;
  }

  if (!tpriv->isoc_framelist) {
    tpriv->num_iso_packets = transfer->num_iso_packets;
    tpriv->isoc_framelist = (IOUSBIsocFrame*) calloc ((size_t)transfer->num_iso_packets, sizeof(IOUSBIsocFrame));
    if (!tpriv->isoc_framelist)
      return LIBUSB_ERROR_NO_MEM;
  }

  /* copy the frame list from the libusb descriptor (the structures differ only is member order) */
  for (i = 0 ; i < transfer->num_iso_packets ; i++) {
    unsigned int length = transfer->iso_packet_desc[i].length;
    assert(length <= UINT16_MAX);
    tpriv->isoc_framelist[i].frReqCount = (UInt16)length;
  }

  /* determine the interface/endpoint to use */
  if (ep_to_pipeRef (transfer->dev_handle, transfer->endpoint, &pipeRef, NULL, &cInterface) != 0) {
    usbi_err (TRANSFER_CTX (transfer), "endpoint not found on any open interface");

    return LIBUSB_ERROR_NOT_FOUND;
  }

  /* determine the properties of this endpoint and the speed of the device */
  (*(cInterface->interface))->GetPipeProperties (cInterface->interface, pipeRef, &direction, &number,
                                                 &transferType, &maxPacketSize, &interval);

  /* Last but not least we need the bus frame number */
  kresult = (*(cInterface->interface))->GetBusFrameNumber(cInterface->interface, &frame, &atTime);
  if (kresult != kIOReturnSuccess) {
    usbi_err (TRANSFER_CTX (transfer), "failed to get bus frame number: %d", kresult);
    free(tpriv->isoc_framelist);
    tpriv->isoc_framelist = NULL;

    return darwin_to_libusb (kresult);
  }

  (*(cInterface->interface))->GetPipeProperties (cInterface->interface, pipeRef, &direction, &number,
                                                 &transferType, &maxPacketSize, &interval);

  /* schedule for a frame a little in the future */
  frame += 4;

  if (cInterface->frames[transfer->endpoint] && frame < cInterface->frames[transfer->endpoint])
    frame = cInterface->frames[transfer->endpoint];

  /* submit the request */
  if (IS_XFERIN(transfer))
    kresult = (*(cInterface->interface))->ReadIsochPipeAsync(cInterface->interface, pipeRef, transfer->buffer, frame,
                                                             (UInt32)transfer->num_iso_packets, tpriv->isoc_framelist, darwin_async_io_callback,
                                                             itransfer);
  else
    kresult = (*(cInterface->interface))->WriteIsochPipeAsync(cInterface->interface, pipeRef, transfer->buffer, frame,
                                                              (UInt32)transfer->num_iso_packets, tpriv->isoc_framelist, darwin_async_io_callback,
                                                              itransfer);

  if (LIBUSB_SPEED_FULL == transfer->dev_handle->dev->speed)
    /* Full speed */
    cInterface->frames[transfer->endpoint] = frame + (UInt32)transfer->num_iso_packets * (1U << (interval - 1));
  else
    /* High/super speed */
    cInterface->frames[transfer->endpoint] = frame + (UInt32)transfer->num_iso_packets * (1U << (interval - 1)) / 8;

  if (kresult != kIOReturnSuccess) {
    usbi_err (TRANSFER_CTX (transfer), "isochronous transfer failed (dir: %s): %s", IS_XFERIN(transfer) ? "In" : "Out",
               darwin_error_str(kresult));
    free (tpriv->isoc_framelist);
    tpriv->isoc_framelist = NULL;
  }

  return darwin_to_libusb (kresult);
}

static int submit_control_transfer(struct usbi_transfer *itransfer) {
  struct libusb_transfer *transfer = USBI_TRANSFER_TO_LIBUSB_TRANSFER(itransfer);
  struct libusb_control_setup *setup = (struct libusb_control_setup *) transfer->buffer;
  struct darwin_cached_device *dpriv = DARWIN_CACHED_DEVICE(transfer->dev_handle->dev);
  struct darwin_transfer_priv *tpriv = usbi_get_transfer_priv(itransfer);

  IOReturn               kresult;

  memset(&tpriv->req, 0, sizeof(tpriv->req));

  /* IOUSBDeviceInterface expects the request in cpu endianness */
  tpriv->req.bmRequestType     = setup->bmRequestType;
  tpriv->req.bRequest          = setup->bRequest;
  /* these values should be in bus order from libusb_fill_control_setup */
  tpriv->req.wValue            = OSSwapLittleToHostInt16 (setup->wValue);
  tpriv->req.wIndex            = OSSwapLittleToHostInt16 (setup->wIndex);
  tpriv->req.wLength           = OSSwapLittleToHostInt16 (setup->wLength);
  /* data is stored after the libusb control block */
  tpriv->req.pData             = transfer->buffer + LIBUSB_CONTROL_SETUP_SIZE;
  tpriv->req.completionTimeout = transfer->timeout;
  tpriv->req.noDataTimeout     = transfer->timeout;

  itransfer->timeout_flags |= USBI_TRANSFER_OS_HANDLES_TIMEOUT;

  /* all transfers in libusb-1.0 are async */

  if (transfer->endpoint) {
    struct darwin_interface *cInterface;
    uint8_t                 pipeRef;

    if (ep_to_pipeRef (transfer->dev_handle, transfer->endpoint, &pipeRef, NULL, &cInterface) != 0) {
      usbi_err (TRANSFER_CTX (transfer), "endpoint not found on any open interface");

      return LIBUSB_ERROR_NOT_FOUND;
    }

    kresult = (*(cInterface->interface))->ControlRequestAsyncTO (cInterface->interface, pipeRef, &(tpriv->req), darwin_async_io_callback, itransfer);
  } else
    /* control request on endpoint 0 */
    kresult = (*(dpriv->device))->DeviceRequestAsyncTO(dpriv->device, &(tpriv->req), darwin_async_io_callback, itransfer);

  if (kresult != kIOReturnSuccess)
    usbi_err (TRANSFER_CTX (transfer), "control request failed: %s", darwin_error_str(kresult));

  return darwin_to_libusb (kresult);
}

static int darwin_submit_transfer(struct usbi_transfer *itransfer) {
  struct libusb_transfer *transfer = USBI_TRANSFER_TO_LIBUSB_TRANSFER(itransfer);

  switch (transfer->type) {
  case LIBUSB_TRANSFER_TYPE_CONTROL:
    return submit_control_transfer(itransfer);
  case LIBUSB_TRANSFER_TYPE_BULK:
  case LIBUSB_TRANSFER_TYPE_INTERRUPT:
    return submit_bulk_transfer(itransfer);
  case LIBUSB_TRANSFER_TYPE_ISOCHRONOUS:
    return submit_iso_transfer(itransfer);
  case LIBUSB_TRANSFER_TYPE_BULK_STREAM:
#if InterfaceVersion >= 550
    return submit_stream_transfer(itransfer);
#else
    usbi_err (TRANSFER_CTX(transfer), "IOUSBFamily version does not support bulk stream transfers");
    return LIBUSB_ERROR_NOT_SUPPORTED;
#endif
  default:
    usbi_err (TRANSFER_CTX(transfer), "unknown endpoint type %d", transfer->type);
    return LIBUSB_ERROR_INVALID_PARAM;
  }
}

static int cancel_control_transfer(struct usbi_transfer *itransfer) {
  struct libusb_transfer *transfer = USBI_TRANSFER_TO_LIBUSB_TRANSFER(itransfer);
  struct darwin_cached_device *dpriv = DARWIN_CACHED_DEVICE(transfer->dev_handle->dev);
  IOReturn kresult;

  usbi_warn (ITRANSFER_CTX (itransfer), "aborting all transactions control pipe");

  if (!dpriv->device)
    return LIBUSB_ERROR_NO_DEVICE;

  kresult = (*(dpriv->device))->USBDeviceAbortPipeZero (dpriv->device);

  return darwin_to_libusb (kresult);
}

static int darwin_abort_transfers (struct usbi_transfer *itransfer) {
  struct libusb_transfer *transfer = USBI_TRANSFER_TO_LIBUSB_TRANSFER(itransfer);
  struct darwin_cached_device *dpriv = DARWIN_CACHED_DEVICE(transfer->dev_handle->dev);
  struct darwin_interface *cInterface;
  uint8_t pipeRef, iface;
  IOReturn kresult;

  if (ep_to_pipeRef (transfer->dev_handle, transfer->endpoint, &pipeRef, &iface, &cInterface) != 0) {
    usbi_err (TRANSFER_CTX (transfer), "endpoint not found on any open interface");

    return LIBUSB_ERROR_NOT_FOUND;
  }

  if (!dpriv->device)
    return LIBUSB_ERROR_NO_DEVICE;

  usbi_warn (ITRANSFER_CTX (itransfer), "aborting all transactions on interface %d pipe %d", iface, pipeRef);

  /* abort transactions */
#if InterfaceVersion >= 550
  if (LIBUSB_TRANSFER_TYPE_BULK_STREAM == transfer->type)
    (*(cInterface->interface))->AbortStreamsPipe (cInterface->interface, pipeRef, itransfer->stream_id);
  else
#endif
    (*(cInterface->interface))->AbortPipe (cInterface->interface, pipeRef);

  usbi_dbg ("calling clear pipe stall to clear the data toggle bit");

  /* newer versions of darwin support clearing additional bits on the device's endpoint */
  kresult = (*(cInterface->interface))->ClearPipeStallBothEnds(cInterface->interface, pipeRef);

  return darwin_to_libusb (kresult);
}

static int darwin_cancel_transfer(struct usbi_transfer *itransfer) {
  struct libusb_transfer *transfer = USBI_TRANSFER_TO_LIBUSB_TRANSFER(itransfer);

  switch (transfer->type) {
  case LIBUSB_TRANSFER_TYPE_CONTROL:
    return cancel_control_transfer(itransfer);
  case LIBUSB_TRANSFER_TYPE_BULK:
  case LIBUSB_TRANSFER_TYPE_INTERRUPT:
  case LIBUSB_TRANSFER_TYPE_ISOCHRONOUS:
    return darwin_abort_transfers (itransfer);
  default:
    usbi_err (TRANSFER_CTX(transfer), "unknown endpoint type %d", transfer->type);
    return LIBUSB_ERROR_INVALID_PARAM;
  }
}

static void darwin_async_io_callback (void *refcon, IOReturn result, void *arg0) {
  struct usbi_transfer *itransfer = (struct usbi_transfer *)refcon;
  struct libusb_transfer *transfer = USBI_TRANSFER_TO_LIBUSB_TRANSFER(itransfer);
  struct darwin_transfer_priv *tpriv = usbi_get_transfer_priv(itransfer);

  usbi_dbg ("an async io operation has completed");

  /* if requested write a zero packet */
  if (kIOReturnSuccess == result && IS_XFEROUT(transfer) && transfer->flags & LIBUSB_TRANSFER_ADD_ZERO_PACKET) {
    struct darwin_interface *cInterface;
    uint8_t pipeRef;

    (void) ep_to_pipeRef (transfer->dev_handle, transfer->endpoint, &pipeRef, NULL, &cInterface);

    (*(cInterface->interface))->WritePipe (cInterface->interface, pipeRef, transfer->buffer, 0);
  }

  tpriv->result = result;
  tpriv->size = (UInt32) (uintptr_t) arg0;

  /* signal the core that this transfer is complete */
  usbi_signal_transfer_completion(itransfer);
}

static enum libusb_transfer_status darwin_transfer_status (struct usbi_transfer *itransfer, IOReturn result) {
  if (itransfer->timeout_flags & USBI_TRANSFER_TIMED_OUT)
    result = kIOUSBTransactionTimeout;

  switch (result) {
  case kIOReturnUnderrun:
  case kIOReturnSuccess:
    return LIBUSB_TRANSFER_COMPLETED;
  case kIOReturnAborted:
    return LIBUSB_TRANSFER_CANCELLED;
  case kIOUSBPipeStalled:
    usbi_dbg ("transfer error: pipe is stalled");
    return LIBUSB_TRANSFER_STALL;
  case kIOReturnOverrun:
    usbi_warn (ITRANSFER_CTX (itransfer), "transfer error: data overrun");
    return LIBUSB_TRANSFER_OVERFLOW;
  case kIOUSBTransactionTimeout:
    usbi_warn (ITRANSFER_CTX (itransfer), "transfer error: timed out");
    itransfer->timeout_flags |= USBI_TRANSFER_TIMED_OUT;
    return LIBUSB_TRANSFER_TIMED_OUT;
  default:
    usbi_warn (ITRANSFER_CTX (itransfer), "transfer error: %s (value = 0x%08x)", darwin_error_str (result), result);
    return LIBUSB_TRANSFER_ERROR;
  }
}

static int darwin_handle_transfer_completion (struct usbi_transfer *itransfer) {
  struct libusb_transfer *transfer = USBI_TRANSFER_TO_LIBUSB_TRANSFER(itransfer);
  struct darwin_transfer_priv *tpriv = usbi_get_transfer_priv(itransfer);
  const unsigned char max_transfer_type = LIBUSB_TRANSFER_TYPE_BULK_STREAM;
  const char *transfer_types[max_transfer_type + 1] = {"control", "isoc", "bulk", "interrupt", "bulk-stream"};
  bool is_isoc = LIBUSB_TRANSFER_TYPE_ISOCHRONOUS == transfer->type;

  if (transfer->type > max_transfer_type) {
    usbi_err (TRANSFER_CTX(transfer), "unknown endpoint type %d", transfer->type);
    return LIBUSB_ERROR_INVALID_PARAM;
  }

  if (NULL == tpriv) {
    usbi_err (TRANSFER_CTX(transfer), "malformed request is missing transfer priv");
    return LIBUSB_ERROR_INVALID_PARAM;
  }

  usbi_dbg ("handling transfer completion type %s with kernel status %d", transfer_types[transfer->type], tpriv->result);

  if (kIOReturnSuccess == tpriv->result || kIOReturnUnderrun == tpriv->result) {
    if (is_isoc && tpriv->isoc_framelist) {
      /* copy isochronous results back */

      for (int i = 0; i < transfer->num_iso_packets ; i++) {
        struct libusb_iso_packet_descriptor *lib_desc = &transfer->iso_packet_desc[i];
        lib_desc->status = darwin_transfer_status (itransfer, tpriv->isoc_framelist[i].frStatus);
        lib_desc->actual_length = tpriv->isoc_framelist[i].frActCount;
      }
    } else if (!is_isoc) {
      itransfer->transferred += tpriv->size;
    }
  }

  /* it is ok to handle cancelled transfers without calling usbi_handle_transfer_cancellation (we catch timeout transfers) */
  return usbi_handle_transfer_completion (itransfer, darwin_transfer_status (itransfer, tpriv->result));
}

#if !defined(HAVE_CLOCK_GETTIME)
void usbi_get_monotonic_time(struct timespec *tp) {
  mach_timespec_t sys_time;

  /* use system boot time as reference for the monotonic clock */
  clock_get_time (clock_monotonic, &sys_time);

  tp->tv_sec  = sys_time.tv_sec;
  tp->tv_nsec = sys_time.tv_nsec;
}

void usbi_get_real_time(struct timespec *tp) {
  mach_timespec_t sys_time;

  /* CLOCK_REALTIME represents time since the epoch */
  clock_get_time (clock_realtime, &sys_time);

  tp->tv_sec  = sys_time.tv_sec;
  tp->tv_nsec = sys_time.tv_nsec;
}
#endif

#if InterfaceVersion >= 550
static int darwin_alloc_streams (struct libusb_device_handle *dev_handle, uint32_t num_streams, unsigned char *endpoints,
                                 int num_endpoints) {
  struct darwin_interface *cInterface;
  UInt32 supportsStreams;
  uint8_t pipeRef;
  int rc, i;

  /* find the minimum number of supported streams on the endpoint list */
  for (i = 0 ; i < num_endpoints ; ++i) {
    if (0 != (rc = ep_to_pipeRef (dev_handle, endpoints[i], &pipeRef, NULL, &cInterface))) {
      return rc;
    }

    (*(cInterface->interface))->SupportsStreams (cInterface->interface, pipeRef, &supportsStreams);
    if (num_streams > supportsStreams)
      num_streams = supportsStreams;
  }

  /* it is an error if any endpoint in endpoints does not support streams */
  if (0 == num_streams)
    return LIBUSB_ERROR_INVALID_PARAM;

  /* create the streams */
  for (i = 0 ; i < num_endpoints ; ++i) {
    (void) ep_to_pipeRef (dev_handle, endpoints[i], &pipeRef, NULL, &cInterface);

    rc = (*(cInterface->interface))->CreateStreams (cInterface->interface, pipeRef, num_streams);
    if (kIOReturnSuccess != rc)
      return darwin_to_libusb(rc);
  }

  assert(num_streams <= INT_MAX);
  return (int)num_streams;
}

static int darwin_free_streams (struct libusb_device_handle *dev_handle, unsigned char *endpoints, int num_endpoints) {
  struct darwin_interface *cInterface;
  UInt32 supportsStreams;
  uint8_t pipeRef;
  int rc;

  for (int i = 0 ; i < num_endpoints ; ++i) {
    if (0 != (rc = ep_to_pipeRef (dev_handle, endpoints[i], &pipeRef, NULL, &cInterface)))
      return rc;

    (*(cInterface->interface))->SupportsStreams (cInterface->interface, pipeRef, &supportsStreams);
    if (0 == supportsStreams)
      return LIBUSB_ERROR_INVALID_PARAM;

    rc = (*(cInterface->interface))->CreateStreams (cInterface->interface, pipeRef, 0);
    if (kIOReturnSuccess != rc)
      return darwin_to_libusb(rc);
  }

  return LIBUSB_SUCCESS;
}
#endif

const struct usbi_os_backend usbi_backend = {
        .name = "Darwin",
        .caps = 0,
        .init = darwin_init,
        .exit = darwin_exit,
        .get_active_config_descriptor = darwin_get_active_config_descriptor,
        .get_config_descriptor = darwin_get_config_descriptor,
        .hotplug_poll = darwin_hotplug_poll,

        .open = darwin_open,
        .close = darwin_close,
        .get_configuration = darwin_get_configuration,
        .set_configuration = darwin_set_configuration,
        .claim_interface = darwin_claim_interface,
        .release_interface = darwin_release_interface,

        .set_interface_altsetting = darwin_set_interface_altsetting,
        .clear_halt = darwin_clear_halt,
        .reset_device = darwin_reset_device,

#if InterfaceVersion >= 550
        .alloc_streams = darwin_alloc_streams,
        .free_streams = darwin_free_streams,
#endif

        .kernel_driver_active = darwin_kernel_driver_active,

        .destroy_device = darwin_destroy_device,

        .submit_transfer = darwin_submit_transfer,
        .cancel_transfer = darwin_cancel_transfer,

        .handle_transfer_completion = darwin_handle_transfer_completion,

        .device_priv_size = sizeof(struct darwin_device_priv),
        .device_handle_priv_size = sizeof(struct darwin_device_handle_priv),
        .transfer_priv_size = sizeof(struct darwin_transfer_priv),
};
