Skip to content

Commit

Permalink
Added libusb_get_device_string() which works with a closed device lib…
Browse files Browse the repository at this point in the history
…usb#866

This new API call provides access to device strings,
normally accessed using USB string descriptors,
without opening the device.  The common use case is
to filter on serial number for selecting one of multiple
connected devices.

The commit adds the new API function, documentation, and support
for the following platforms:

* Windows WinUSB
* Linux
* macOS

All platforms support manufacturer, product, and
serial number strings.

The example/listdevs.c now has a "--verbose" command line argument
that prints these strings.

Known issues:

* On one Ubuntu Linux machine, reading the /sys/bus/usb/ files
  to get the strings takes a variable amount of time on one device,
  an xHCI root hub.  While the read can be fast, it can also take
  seconds.  Using "cat" at the command line has the exact same
  behavior.

References:

* libusb#866
* libusb#875
* libusb#1258
* hjmallon@d0f9fd4

Thank you to the prior work that made this much quicker:

* @mcuee
* @sonatique
* @hjmallon

I apologize anyone that I missed.  This feature has been in the works
for a long time with multiple starts and stops.

And thank you to @Youw for providing quick, detailed feedback!

Signed-off-by: Matt Liberty <matt.liberty@jetperch.com>
  • Loading branch information
mliberty1 committed Jul 10, 2024
1 parent e678b3f commit 1b11149
Show file tree
Hide file tree
Showing 12 changed files with 439 additions and 4 deletions.
47 changes: 43 additions & 4 deletions examples/listdevs.c
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,17 @@
*/

#include <stdio.h>

#include <string.h>
#include "libusb.h"

static void print_devs(libusb_device **devs)
#define ARG_CONSUME() --argc; ++argv

static void print_devs(libusb_device **devs, int verbose)
{
libusb_device *dev;
int i = 0, j = 0;
uint8_t path[8];
char string_buffer[LIBUSB_DEVICE_STRING_UTF8_BYTES_MAX];

while ((dev = devs[i++]) != NULL) {
struct libusb_device_descriptor desc;
Expand All @@ -45,16 +48,52 @@ static void print_devs(libusb_device **devs)
for (j = 1; j < r; j++)
printf(".%d", path[j]);
}

if (verbose) {
r = libusb_get_device_string(dev, LIBUSB_DEVICE_STRING_MANUFACTURER,
string_buffer, sizeof(string_buffer));
if (r >= 0) {
printf("\n manufacturer = %s", string_buffer);
}

r = libusb_get_device_string(dev, LIBUSB_DEVICE_STRING_PRODUCT,
string_buffer, sizeof(string_buffer));
if (r >= 0) {
printf("\n product = %s", string_buffer);
}

r = libusb_get_device_string(dev, LIBUSB_DEVICE_STRING_SERIAL_NUMBER,
string_buffer, sizeof(string_buffer));
if (r >= 0) {
printf("\n serial_number = %s", string_buffer);
}
}
printf("\n");
}
}

int main(void)
static int usage(void) {
printf("usage: listdevs [--verbose]\n");
return 1;
}

int main(int argc, char * argv[])
{
int verbose = 0;
libusb_device **devs;
int r;
ssize_t cnt;

ARG_CONSUME();
while (argc) {
if ((0 == strcmp("-v", argv[0])) || (0 == strcmp("--verbose", argv[0]))) {
++verbose;
ARG_CONSUME();
} else {
return usage();
}
}

r = libusb_init_context(/*ctx=*/NULL, /*options=*/NULL, /*num_options=*/0);
if (r < 0)
return r;
Expand All @@ -65,7 +104,7 @@ int main(void)
return (int) cnt;
}

print_devs(devs);
print_devs(devs, verbose);
libusb_free_device_list(devs, 1);

libusb_exit(NULL);
Expand Down
8 changes: 8 additions & 0 deletions libusb/core.c
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,7 @@ if (cfg != desired)
* - libusb_get_device_descriptor()
* - libusb_get_device_list()
* - libusb_get_device_speed()
* - libusb_get_device_string()
* - libusb_get_iso_packet_buffer()
* - libusb_get_iso_packet_buffer_simple()
* - libusb_get_max_alt_packet_size()
Expand Down Expand Up @@ -499,6 +500,7 @@ if (cfg != desired)
* - \ref libusb_capability
* - \ref libusb_class_code
* - \ref libusb_descriptor_type
* - \ref libusb_device_string_type
* - \ref libusb_endpoint_direction
* - \ref libusb_endpoint_transfer_type
* - \ref libusb_error
Expand Down Expand Up @@ -1308,6 +1310,12 @@ void API_EXPORTED libusb_unref_device(libusb_device *dev)
usbi_disconnect_device(dev);
}

for (int idx = 0; idx < LIBUSB_DEVICE_STRING_COUNT; ++idx) {
if (NULL != dev->device_strings_utf8[idx]) {
free(dev->device_strings_utf8[idx]);
}
}

free(dev);
}
}
Expand Down
87 changes: 87 additions & 0 deletions libusb/descriptor.c
Original file line number Diff line number Diff line change
Expand Up @@ -1517,3 +1517,90 @@ void API_EXPORTED libusb_free_interface_association_descriptors(
free((void*)iad_array->iad);
free(iad_array);
}

/** \ingroup libusb_desc
* Retrieve a device string without needing to open the device.
*
* \param dev the target device
* \param string_type the string type to retrieve
* \param data the data buffer for the UTF-8 encoded string.
* \param length the size of the data buffer in bytes.
* USB string descriptors cannot be longer than
* LIBUSB_DEVICE_STRING_UTF8_BYTES_MAX.
* \returns an error code or
* the actual string length in bytes including the null terminator.
* \see libusb_get_string_descriptor()
* \see libusb_get_string_descriptor_ascii()
*
* This function works when the device is still closed since it relies
* on the operating system to provide the string. The operating system
* normally reads and caches the common string descriptors during
* USB enumeration.
*
* Since the USB string descriptor could be processed by the OS,
* this function returns a UTF-8 encoded string. The string will
* be returned untranslated or in the default OS language
* when supported by the OS and USB device.
*
* One way to call this function is using a buffer on the stack:
*
* char buffer[LIBUSB_DEVICE_STRING_UTF8_BYTES_MAX];
* int ret = libusb_get_device_string(dev, LIBUSB_DEVICE_STRING_SERIAL_NUMBER, buffer, sizeof(buffer));
* if (ret < 0) {
* // handle error
* }
*
* This function is commonly used to get the serial number to allow
* for device selection before opening the selected device.
*/
int API_EXPORTED libusb_get_device_string(libusb_device* dev,
enum libusb_device_string_type string_type, char* data, int length) {
char * s;
if ((string_type < 0) || (string_type >= LIBUSB_DEVICE_STRING_COUNT)) {
return LIBUSB_ERROR_INVALID_PARAM;
}
if ((NULL != data) && length) {
*data = 0; // return an empty string on errors when possible
}

if (NULL == dev->device_strings_utf8[string_type]) {
if (usbi_backend.get_device_string) {
s = malloc(LIBUSB_DEVICE_STRING_UTF8_BYTES_MAX);
int rv = usbi_backend.get_device_string(dev, string_type, s, LIBUSB_DEVICE_STRING_UTF8_BYTES_MAX);
if (rv < 0) {
free(s);
return rv;
} else {
dev->device_strings_utf8[string_type] = s;
}
} else {
return LIBUSB_ERROR_NOT_SUPPORTED;
}
}

s = dev->device_strings_utf8[string_type];
if (NULL == s) {
return LIBUSB_ERROR_NOT_SUPPORTED;
}

// copy UTF-8 string and compute length
int k = 0;
for (k = 0; s[k]; ++k) {
if ((NULL != data) && (k < length)) {
data[k] = s[k];
}
}
if ((NULL == data) || (0 == length)) {
// do nothing
} else if (k >= length) {
// truncate respecting UTF-8 character boundaries
int idx = length - 1;
while (idx && (0x80 == (data[idx] & 0xC0))) { // utf-8 continuation byte
--idx;
}
data[idx] = 0;
} else {
data[k] = 0;
}
return k;
}
2 changes: 2 additions & 0 deletions libusb/libusb-1.0.def
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ EXPORTS
libusb_get_device_list@8 = libusb_get_device_list
libusb_get_device_speed
libusb_get_device_speed@4 = libusb_get_device_speed
libusb_get_device_string
libusb_get_device_string@16 = libusb_get_device_string
libusb_get_interface_association_descriptors
libusb_get_interface_association_descriptors@12 = libusb_get_interface_association_descriptors
libusb_get_max_alt_packet_size
Expand Down
23 changes: 23 additions & 0 deletions libusb/libusb.h
Original file line number Diff line number Diff line change
Expand Up @@ -1647,6 +1647,27 @@ enum libusb_option {
LIBUSB_OPTION_MAX = 4
};

/** \ingroup libusb_desc
* The device string type.
*/
enum libusb_device_string_type {
LIBUSB_DEVICE_STRING_MANUFACTURER,
LIBUSB_DEVICE_STRING_PRODUCT,
LIBUSB_DEVICE_STRING_SERIAL_NUMBER,
LIBUSB_DEVICE_STRING_COUNT // The total number of string types.
};

/** \ingroup libusb_desc
* The maximum length for a device string descriptor in UTF-8.
*
* 255 max descriptor length with 2 byte header
* => 253 bytes UTF-16LE
* => 126 codepoints, but last codepoint is 0
* => 125 * 3 + 2
* => 376 bytes
*/
#define LIBUSB_DEVICE_STRING_UTF8_BYTES_MAX (384U)

/** \ingroup libusb_lib
* Callback function for handling log messages.
* \param ctx the context which is related to the log message, or NULL if it
Expand Down Expand Up @@ -1694,6 +1715,8 @@ void LIBUSB_CALL libusb_free_device_list(libusb_device **list,
libusb_device * LIBUSB_CALL libusb_ref_device(libusb_device *dev);
void LIBUSB_CALL libusb_unref_device(libusb_device *dev);

int LIBUSB_CALL libusb_get_device_string(libusb_device* dev,
enum libusb_device_string_type string_type, char * data, int length);
int LIBUSB_CALL libusb_get_configuration(libusb_device_handle *dev,
int *config);
int LIBUSB_CALL libusb_get_device_descriptor(libusb_device *dev,
Expand Down
25 changes: 25 additions & 0 deletions libusb/libusbi.h
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,8 @@ struct libusb_device {

struct libusb_device_descriptor device_descriptor;
usbi_atomic_t attached;

char * device_strings_utf8[LIBUSB_DEVICE_STRING_COUNT];
};

struct libusb_device_handle {
Expand Down Expand Up @@ -1021,6 +1023,29 @@ struct usbi_os_backend {
int (*get_device_list)(struct libusb_context *ctx,
struct discovered_devs **discdevs);

/* Retrieve a device string without needing to open the device.
*
* The string should be retrieved without opening the device
* and ideally without performing USB transactions to the device.
* Most operating systems read and cache the common string
* descriptors. Use the OS-specific calls to retrieve these strings.
*
* Since the USB string descriptor could be processed by the OS,
* this function returns a UTF-8 encoded string. The string will
* be returned untranslated or in the default OS language
* when supported by the OS and USB device.
*
* This function must not write more than length bytes into data,
* including the null terminator.
*
* Return:
* - The actual length in bytes excluding the null termintor on success.
* - LIBUSB_ERROR_NO_DEVICE if device not found.
* - LIBUSB_ERROR_INVALID_PARAM for invalid string types.
*/
int (*get_device_string)(libusb_device* dev,
enum libusb_device_string_type string_type, char* data, int length);

/* Apps which were written before hotplug support, may listen for
* hotplug events on their own and call libusb_get_device_list on
* device addition. In this case libusb_get_device_list will likely
Expand Down
48 changes: 48 additions & 0 deletions libusb/os/darwin_usb.c
Original file line number Diff line number Diff line change
Expand Up @@ -940,6 +940,53 @@ static void darwin_exit (struct libusb_context *ctx) {
usbi_mutex_unlock(&darwin_cached_devices_mutex);
}

static int darwin_get_device_string(struct libusb_device *dev,
enum libusb_device_string_type string_type, char *buffer, int length) {

struct darwin_cached_device *priv = DARWIN_CACHED_DEVICE(dev);
io_iterator_t deviceIterator;
io_service_t service;
kern_return_t kresult;
CFStringRef cf;

kresult = usb_setup_device_iterator (&deviceIterator, priv->location);
if (kresult != kIOReturnSuccess)
return darwin_to_libusb (kresult);

service = IOIteratorNext (deviceIterator);
IOObjectRelease(deviceIterator);
if (service == IO_OBJECT_NULL)
return LIBUSB_ERROR_NOT_FOUND;

switch (string_type) {
case LIBUSB_DEVICE_STRING_MANUFACTURER:
cf = IORegistryEntryCreateCFProperty(service, CFSTR(kUSBVendorString), kCFAllocatorDefault, 0);
break;
case LIBUSB_DEVICE_STRING_PRODUCT:
cf = IORegistryEntryCreateCFProperty(service, CFSTR(kUSBProductString), kCFAllocatorDefault, 0);
break;
case LIBUSB_DEVICE_STRING_SERIAL_NUMBER:
cf = IORegistryEntryCreateCFProperty(service, CFSTR(kUSBSerialNumberString), kCFAllocatorDefault, 0);
break;
default:
return LIBUSB_ERROR_INVALID_PARAM;
}

IOObjectRelease(service);
if (cf == NULL)
return LIBUSB_ERROR_NOT_FOUND;

long cfUsedIndex = 0;
CFStringGetBytes(cf, CFRangeMake(0, CFStringGetLength(cf)), kCFStringEncodingUTF8, '?', false,
(uint8_t *) buffer, length, &cfUsedIndex);
CFRelease(cf);

if (cfUsedIndex <= 0)
return LIBUSB_ERROR_NOT_FOUND;

return (int) cfUsedIndex;
}

static int get_configuration_index (struct libusb_device *dev, UInt8 config_value) {
struct darwin_cached_device *priv = DARWIN_CACHED_DEVICE(dev);
UInt8 i, numConfig;
Expand Down Expand Up @@ -2900,6 +2947,7 @@ const struct usbi_os_backend usbi_backend = {
.exit = darwin_exit,
.set_option = NULL,
.get_device_list = NULL,
.get_device_string = darwin_get_device_string,
.hotplug_poll = darwin_hotplug_poll,
.wrap_sys_device = NULL,
.open = darwin_open,
Expand Down
Loading

0 comments on commit 1b11149

Please sign in to comment.