libfprint/libfprint/fpi-usb-transfer.c

551 lines
18 KiB
C

/*
* FPrint USB transfer handling
* Copyright (C) 2019 Benjamin Berg <bberg@redhat.com>
*
* 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 "fpi-usb-transfer.h"
/**
* SECTION:fpi-usb-transfer
* @title: USB transfer helpers
* @short_description: Helpers for libgusb to ease transfer handling
*
* #FpiUsbTransfer is a structure to simplify the USB transfer handling.
* The main goal is to ease memory management and provide more parameters
* to callbacks that are useful for libfprint drivers.
*
* Drivers should use this API only rather than accessing the GUsbDevice
* directly in most cases.
*/
G_DEFINE_BOXED_TYPE (FpiUsbTransfer, fpi_usb_transfer, fpi_usb_transfer_ref, fpi_usb_transfer_unref)
static void
log_transfer (FpiUsbTransfer *transfer, gboolean submit, GError *error)
{
if (g_getenv ("FP_DEBUG_TRANSFER"))
{
if (!submit)
{
g_autofree gchar *error_str = NULL;
if (error)
error_str = g_strdup_printf ("with error (%s)", error->message);
else
error_str = g_strdup ("successfully");
g_debug ("Transfer %p completed %s, requested length %zd, actual length %zd, endpoint 0x%x",
transfer,
error_str,
transfer->length,
transfer->actual_length,
transfer->endpoint);
}
else
{
g_debug ("Transfer %p submitted, requested length %zd, endpoint 0x%x",
transfer,
transfer->length,
transfer->endpoint);
}
if (!submit == !!(transfer->endpoint & FPI_USB_ENDPOINT_IN))
{
g_autoptr(GString) line = NULL;
gssize dump_len;
dump_len = (transfer->endpoint & FPI_USB_ENDPOINT_IN) ? transfer->actual_length : transfer->length;
line = g_string_new ("");
/* Dump the buffer. */
for (gint i = 0; i < dump_len; i++)
{
g_string_append_printf (line, "%02x ", transfer->buffer[i]);
if ((i + 1) % 16 == 0)
{
g_debug ("%s", line->str);
g_string_set_size (line, 0);
}
}
if (line->len)
g_debug ("%s", line->str);
}
}
}
/**
* fpi_usb_transfer_new:
* @device: The #FpDevice the transfer is for
*
* Creates a new #FpiUsbTransfer.
*
* Returns: (transfer full): A newly created #FpiUsbTransfer
*/
FpiUsbTransfer *
fpi_usb_transfer_new (FpDevice * device)
{
FpiUsbTransfer *self;
g_assert (device != NULL);
self = g_slice_new0 (FpiUsbTransfer);
self->ref_count = 1;
self->type = FP_TRANSFER_NONE;
self->device = device;
return self;
}
static void
fpi_usb_transfer_free (FpiUsbTransfer *self)
{
g_assert (self);
g_assert_cmpint (self->ref_count, ==, 0);
if (self->free_buffer && self->buffer)
self->free_buffer (self->buffer);
self->buffer = NULL;
g_slice_free (FpiUsbTransfer, self);
}
/**
* fpi_usb_transfer_ref:
* @self: A #FpiUsbTransfer
*
* Increments the reference count of @self by one.
*
* Returns: (transfer full): @self
*/
FpiUsbTransfer *
fpi_usb_transfer_ref (FpiUsbTransfer *self)
{
g_return_val_if_fail (self, NULL);
g_return_val_if_fail (self->ref_count, NULL);
g_atomic_int_inc (&self->ref_count);
return self;
}
/**
* fpi_usb_transfer_unref:
* @self: A #FpiUsbTransfer
*
* Decrements the reference count of @self by one, freeing the structure when
* the reference count reaches zero.
*/
void
fpi_usb_transfer_unref (FpiUsbTransfer *self)
{
g_return_if_fail (self);
g_return_if_fail (self->ref_count);
if (g_atomic_int_dec_and_test (&self->ref_count))
fpi_usb_transfer_free (self);
}
/**
* fpi_usb_transfer_fill_bulk:
* @transfer: The #FpiUsbTransfer
* @endpoint: The endpoint to send the transfer to
* @length: The buffer size to allocate
*
* Prepare a bulk transfer. A buffer will be created for you, use
* fpi_usb_transfer_fill_bulk_full() if you want to send a static buffer
* or receive a pre-defined buffer.
*/
void
fpi_usb_transfer_fill_bulk (FpiUsbTransfer *transfer,
guint8 endpoint,
gsize length)
{
fpi_usb_transfer_fill_bulk_full (transfer,
endpoint,
g_malloc0 (length),
length,
g_free);
}
/**
* fpi_usb_transfer_fill_bulk_full:
* @transfer: The #FpiUsbTransfer
* @endpoint: The endpoint to send the transfer to
* @buffer: The data to send.
* @length: The size of @buffer
* @free_func: (destroy buffer): Destroy notify for @buffer
*
* Prepare a bulk transfer.
*/
void
fpi_usb_transfer_fill_bulk_full (FpiUsbTransfer *transfer,
guint8 endpoint,
guint8 *buffer,
gsize length,
GDestroyNotify free_func)
{
g_assert (transfer->type == FP_TRANSFER_NONE);
g_assert (buffer != NULL);
transfer->type = FP_TRANSFER_BULK;
transfer->endpoint = endpoint;
transfer->buffer = buffer;
transfer->length = length;
transfer->free_buffer = free_func;
}
/**
* fpi_usb_transfer_fill_control:
* @transfer: The #FpiUsbTransfer
* @direction: The direction of the control transfer
* @request_type: The request type
* @recipient: The recipient
* @request: The control transfer request
* @value: The control transfer value
* @idx: The control transfer index
* @length: The size of the transfer
*
* Prepare a control transfer. The function will create a new buffer,
* you can initialize the buffer after calling this function.
*/
void
fpi_usb_transfer_fill_control (FpiUsbTransfer *transfer,
GUsbDeviceDirection direction,
GUsbDeviceRequestType request_type,
GUsbDeviceRecipient recipient,
guint8 request,
guint16 value,
guint16 idx,
gsize length)
{
g_assert (transfer->type == FP_TRANSFER_NONE);
transfer->type = FP_TRANSFER_CONTROL;
transfer->direction = direction;
transfer->request_type = request_type;
transfer->recipient = recipient;
transfer->request = request;
transfer->value = value;
transfer->idx = idx;
transfer->length = length;
transfer->buffer = g_malloc0 (length);
transfer->free_buffer = g_free;
}
/**
* fpi_usb_transfer_fill_interrupt:
* @transfer: The #FpiUsbTransfer
* @endpoint: The endpoint to send the transfer to
* @length: The size of the transfer
*
* Prepare an interrupt transfer. The function will create a new buffer,
* you can initialize the buffer after calling this function.
*/
void
fpi_usb_transfer_fill_interrupt (FpiUsbTransfer *transfer,
guint8 endpoint,
gsize length)
{
fpi_usb_transfer_fill_interrupt_full (transfer,
endpoint,
g_malloc0 (length),
length,
g_free);
}
/**
* fpi_usb_transfer_fill_interrupt_full:
* @transfer: The #FpiUsbTransfer
* @endpoint: The endpoint to send the transfer to
* @buffer: The data to send.
* @length: The size of @buffer
* @free_func: (destroy buffer): Destroy notify for @buffer
*
* Prepare an interrupt transfer.
*/
void
fpi_usb_transfer_fill_interrupt_full (FpiUsbTransfer *transfer,
guint8 endpoint,
guint8 *buffer,
gsize length,
GDestroyNotify free_func)
{
g_assert (transfer->type == FP_TRANSFER_NONE);
g_assert (buffer != NULL);
transfer->type = FP_TRANSFER_INTERRUPT;
transfer->endpoint = endpoint;
transfer->buffer = buffer;
transfer->length = length;
transfer->free_buffer = free_func;
}
static void
transfer_finish_cb (GObject *source_object, GAsyncResult *res, gpointer user_data)
{
GError *error = NULL;
FpiUsbTransfer *transfer = user_data;
FpiUsbTransferCallback callback;
switch (transfer->type)
{
case FP_TRANSFER_BULK:
transfer->actual_length =
g_usb_device_bulk_transfer_finish (G_USB_DEVICE (source_object),
res,
&error);
break;
case FP_TRANSFER_CONTROL:
transfer->actual_length =
g_usb_device_control_transfer_finish (G_USB_DEVICE (source_object),
res,
&error);
break;
case FP_TRANSFER_INTERRUPT:
transfer->actual_length =
g_usb_device_interrupt_transfer_finish (G_USB_DEVICE (source_object),
res,
&error);
break;
case FP_TRANSFER_NONE:
default:
g_assert_not_reached ();
}
log_transfer (transfer, FALSE, error);
/* Check for short error, and set an error if requested */
if (error == NULL &&
transfer->short_is_error &&
transfer->actual_length > 0 &&
transfer->actual_length != transfer->length)
{
error = g_error_new (G_USB_DEVICE_ERROR,
G_USB_DEVICE_ERROR_IO,
"Unexpected short error of %zd size (expected %zd)", transfer->actual_length, transfer->length);
}
callback = transfer->callback;
transfer->callback = NULL;
callback (transfer, transfer->device, transfer->user_data, error);
fpi_usb_transfer_unref (transfer);
}
static void
transfer_cancel_cb (FpDevice *device, gpointer user_data)
{
FpiUsbTransfer *transfer = user_data;
GError *error;
FpiUsbTransferCallback callback;
error = g_error_new_literal (G_IO_ERROR,
G_IO_ERROR_CANCELLED,
"Transfer was cancelled before being started");
callback = transfer->callback;
transfer->callback = NULL;
transfer->actual_length = -1;
callback (transfer, transfer->device, transfer->user_data, error);
fpi_usb_transfer_unref (transfer);
}
/**
* fpi_usb_transfer_submit:
* @transfer: (transfer full): The transfer to submit, must have been filled.
* @timeout_ms: Timeout for the transfer in ms
* @cancellable: Cancellable to use, e.g. fpi_device_get_cancellable()
* @callback: Callback on completion or error
* @user_data: Data to pass to callback
*
* Submit a USB transfer with a specific timeout and callback functions.
*
* Note that #FpiUsbTransfer will be stolen when this function is called.
* So that all associated data will be free'ed automatically, after the
* callback ran unless fpi_usb_transfer_ref() is explicitly called.
*/
void
fpi_usb_transfer_submit (FpiUsbTransfer *transfer,
guint timeout_ms,
GCancellable *cancellable,
FpiUsbTransferCallback callback,
gpointer user_data)
{
g_return_if_fail (transfer);
g_return_if_fail (callback);
/* Recycling is allowed, but not two at the same time. */
g_return_if_fail (transfer->callback == NULL);
transfer->callback = callback;
transfer->user_data = user_data;
log_transfer (transfer, TRUE, NULL);
/* Work around libgusb cancellation issue, see
* https://github.com/hughsie/libgusb/pull/42
* should be fixed with libgusb 0.3.7.
* Note that this is not race free, we rely on libfprint and API users
* not cancelling from a different thread here.
*/
if (cancellable && g_cancellable_is_cancelled (cancellable))
{
fpi_device_add_timeout (transfer->device, 0,
transfer_cancel_cb, transfer, NULL);
return;
}
switch (transfer->type)
{
case FP_TRANSFER_BULK:
g_usb_device_bulk_transfer_async (fpi_device_get_usb_device (transfer->device),
transfer->endpoint,
transfer->buffer,
transfer->length,
timeout_ms,
cancellable,
transfer_finish_cb,
transfer);
break;
case FP_TRANSFER_CONTROL:
g_usb_device_control_transfer_async (fpi_device_get_usb_device (transfer->device),
transfer->direction,
transfer->request_type,
transfer->recipient,
transfer->request,
transfer->value,
transfer->idx,
transfer->buffer,
transfer->length,
timeout_ms,
cancellable,
transfer_finish_cb,
transfer);
break;
case FP_TRANSFER_INTERRUPT:
g_usb_device_interrupt_transfer_async (fpi_device_get_usb_device (transfer->device),
transfer->endpoint,
transfer->buffer,
transfer->length,
timeout_ms,
cancellable,
transfer_finish_cb,
transfer);
break;
case FP_TRANSFER_NONE:
default:
fpi_usb_transfer_unref (transfer);
g_return_if_reached ();
}
}
/**
* fpi_usb_transfer_submit_sync:
* @transfer: The transfer to submit, must have been filled.
* @timeout_ms: Timeout for the transfer in millisecnods
* @error: Location to store #GError to
*
* Synchronously submit a USB transfer with a specific timeout.
* Only use this function with short timeouts as the application will
* be blocked otherwise.
*
* Note that you still need to fpi_usb_transfer_unref() the
* #FpiUsbTransfer afterwards.
*
* Returns: #TRUE on success, otherwise #FALSE and @error will be set
*/
gboolean
fpi_usb_transfer_submit_sync (FpiUsbTransfer *transfer,
guint timeout_ms,
GError **error)
{
gboolean res;
gsize actual_length;
g_return_val_if_fail (transfer, FALSE);
/* Recycling is allowed, but not two at the same time. */
g_return_val_if_fail (transfer->callback == NULL, FALSE);
log_transfer (transfer, TRUE, NULL);
switch (transfer->type)
{
case FP_TRANSFER_BULK:
res = g_usb_device_bulk_transfer (fpi_device_get_usb_device (transfer->device),
transfer->endpoint,
transfer->buffer,
transfer->length,
&actual_length,
timeout_ms,
NULL,
error);
break;
case FP_TRANSFER_CONTROL:
res = g_usb_device_control_transfer (fpi_device_get_usb_device (transfer->device),
transfer->direction,
transfer->request_type,
transfer->recipient,
transfer->request,
transfer->value,
transfer->idx,
transfer->buffer,
transfer->length,
&actual_length,
timeout_ms,
NULL,
error);
break;
case FP_TRANSFER_INTERRUPT:
res = g_usb_device_interrupt_transfer (fpi_device_get_usb_device (transfer->device),
transfer->endpoint,
transfer->buffer,
transfer->length,
&actual_length,
timeout_ms,
NULL,
error);
break;
case FP_TRANSFER_NONE:
default:
g_return_val_if_reached (FALSE);
}
log_transfer (transfer, FALSE, *error);
if (!res)
transfer->actual_length = -1;
else
transfer->actual_length = actual_length;
return res;
}