mirror of
https://gitlab.freedesktop.org/pipewire/wireplumber.git
synced 2025-12-20 06:30:04 +01:00
Cancel the async calls that get the name of the application owning the service, when WpReserveDevice is finalized or we are going to make another call. Fixes UAF accessing self when the async callback runs.
544 lines
17 KiB
C
544 lines
17 KiB
C
/* WirePlumber
|
|
*
|
|
* Copyright © 2021 Collabora Ltd.
|
|
* @author George Kiagiadakis <george.kiagiadakis@collabora.com>
|
|
*
|
|
* SPDX-License-Identifier: MIT
|
|
*/
|
|
|
|
#include "reserve-device.h"
|
|
#include "plugin.h"
|
|
#include "transitions.h"
|
|
#include "reserve-device-interface.h"
|
|
#include "reserve-device-enums.h"
|
|
|
|
/*
|
|
* WpReserveDevice:
|
|
*/
|
|
G_DEFINE_TYPE (WpReserveDevice, wp_reserve_device, G_TYPE_OBJECT)
|
|
|
|
enum
|
|
{
|
|
ACTION_ACQUIRE,
|
|
ACTION_RELEASE,
|
|
ACTION_DENY_RELEASE,
|
|
SIGNAL_RELEASE_REQUESTED,
|
|
LAST_SIGNAL
|
|
};
|
|
|
|
enum
|
|
{
|
|
PROP_0,
|
|
PROP_PLUGIN,
|
|
PROP_NAME,
|
|
PROP_APP_NAME,
|
|
PROP_APP_DEV_NAME,
|
|
PROP_PRIORITY,
|
|
PROP_STATE,
|
|
PROP_OWNER_APP_NAME,
|
|
};
|
|
|
|
static guint signals[LAST_SIGNAL] = { 0 };
|
|
|
|
static void
|
|
wp_reserve_device_init (WpReserveDevice * self)
|
|
{
|
|
g_weak_ref_init (&self->plugin, NULL);
|
|
g_weak_ref_init (&self->transition, NULL);
|
|
}
|
|
|
|
static void
|
|
on_got_proxy (GObject * src, GAsyncResult * res, WpReserveDevice *self)
|
|
{
|
|
g_autoptr (GError) error = NULL;
|
|
g_autoptr (WpOrgFreedesktopReserveDevice1) proxy =
|
|
wp_org_freedesktop_reserve_device1_proxy_new_finish (res, &error);
|
|
|
|
if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
|
|
/* Operation canceled: self may be invalid by now. */
|
|
return;
|
|
}
|
|
|
|
g_clear_object (&self->get_owner_call);
|
|
|
|
if (!proxy) {
|
|
wp_info_object (self, "%s: Could not get proxy of remote reservation: %s",
|
|
self->name, error->message);
|
|
return;
|
|
}
|
|
|
|
wp_debug_object (self, "%s owned by: %s", self->name,
|
|
wp_org_freedesktop_reserve_device1_get_application_name (proxy));
|
|
|
|
/* ensure that we are still busy and there is no owner_app_name */
|
|
if (self->state == WP_RESERVE_DEVICE_STATE_BUSY && !self->owner_app_name) {
|
|
self->owner_app_name =
|
|
wp_org_freedesktop_reserve_device1_dup_application_name (proxy);
|
|
g_object_notify (G_OBJECT (self), "owner-application-name");
|
|
}
|
|
}
|
|
|
|
static void
|
|
update_owner_app_name (WpReserveDevice *self)
|
|
{
|
|
g_cancellable_cancel (self->get_owner_call);
|
|
g_clear_object (&self->get_owner_call);
|
|
|
|
if (self->state == WP_RESERVE_DEVICE_STATE_BUSY && !self->owner_app_name) {
|
|
/* create proxy */
|
|
g_autoptr (WpReserveDevicePlugin) plugin = g_weak_ref_get (&self->plugin);
|
|
g_autoptr (GDBusConnection) conn = NULL;
|
|
g_object_get (plugin->dbus, "connection", &conn, NULL);
|
|
|
|
self->get_owner_call = g_cancellable_new ();
|
|
wp_org_freedesktop_reserve_device1_proxy_new (conn,
|
|
G_DBUS_PROXY_FLAGS_DO_NOT_CONNECT_SIGNALS |
|
|
G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START,
|
|
self->service_name, self->object_path, self->get_owner_call,
|
|
(GAsyncReadyCallback) on_got_proxy, self);
|
|
}
|
|
else if (self->state != WP_RESERVE_DEVICE_STATE_BUSY && self->owner_app_name) {
|
|
g_clear_pointer (&self->owner_app_name, g_free);
|
|
g_object_notify (G_OBJECT (self), "owner-application-name");
|
|
}
|
|
}
|
|
|
|
static void
|
|
on_name_appeared (GDBusConnection *connection, const gchar *name,
|
|
const gchar *owner, gpointer user_data)
|
|
{
|
|
WpReserveDevice *self = WP_RESERVE_DEVICE (user_data);
|
|
g_autoptr (WpTransition) t = g_weak_ref_get (&self->transition);
|
|
|
|
if (!t || wp_transition_get_completed (t)) {
|
|
self->state = WP_RESERVE_DEVICE_STATE_BUSY;
|
|
wp_info_object (self, "%s busy (by %s)", name, owner);
|
|
g_object_notify (G_OBJECT (self), "state");
|
|
update_owner_app_name (self);
|
|
}
|
|
}
|
|
|
|
static void
|
|
on_name_vanished (GDBusConnection *connection, const gchar *name,
|
|
gpointer user_data)
|
|
{
|
|
WpReserveDevice *self = WP_RESERVE_DEVICE (user_data);
|
|
g_autoptr (WpTransition) t = g_weak_ref_get (&self->transition);
|
|
|
|
if (!t || wp_transition_get_completed (t)) {
|
|
self->state = WP_RESERVE_DEVICE_STATE_AVAILABLE;
|
|
wp_info_object (self, "%s released", name);
|
|
g_object_notify (G_OBJECT (self), "state");
|
|
update_owner_app_name (self);
|
|
}
|
|
}
|
|
|
|
static void
|
|
wp_reserve_device_constructed (GObject * object)
|
|
{
|
|
WpReserveDevice *self = WP_RESERVE_DEVICE (object);
|
|
g_autoptr (WpReserveDevicePlugin) plugin = g_weak_ref_get (&self->plugin);
|
|
g_autoptr (GDBusConnection) conn = NULL;
|
|
|
|
g_object_get (plugin->dbus, "connection", &conn, NULL);
|
|
|
|
self->service_name =
|
|
g_strdup_printf (FDO_RESERVE_DEVICE1_SERVICE ".%s", self->name);
|
|
self->object_path =
|
|
g_strdup_printf (FDO_RESERVE_DEVICE1_PATH "/%s", self->name);
|
|
|
|
/* Watch for the name */
|
|
self->watcher_id = g_bus_watch_name_on_connection (conn,
|
|
self->service_name, G_BUS_NAME_WATCHER_FLAGS_NONE,
|
|
on_name_appeared, on_name_vanished, self, NULL);
|
|
|
|
G_OBJECT_CLASS (wp_reserve_device_parent_class)->constructed (object);
|
|
}
|
|
|
|
static void
|
|
wp_reserve_device_finalize (GObject * object)
|
|
{
|
|
WpReserveDevice *self = WP_RESERVE_DEVICE (object);
|
|
|
|
if (self->watcher_id > 0)
|
|
g_bus_unwatch_name (self->watcher_id);
|
|
if (self->owner_id > 0)
|
|
g_bus_unown_name (self->owner_id);
|
|
|
|
g_cancellable_cancel (self->get_owner_call);
|
|
g_clear_object (&self->get_owner_call);
|
|
|
|
g_weak_ref_clear (&self->plugin);
|
|
g_weak_ref_clear (&self->transition);
|
|
g_clear_pointer (&self->name, g_free);
|
|
g_clear_pointer (&self->app_name, g_free);
|
|
g_clear_pointer (&self->app_dev_name, g_free);
|
|
g_clear_pointer (&self->service_name, g_free);
|
|
g_clear_pointer (&self->object_path, g_free);
|
|
|
|
G_OBJECT_CLASS (wp_reserve_device_parent_class)->finalize (object);
|
|
}
|
|
|
|
static void
|
|
on_acquire_transition_done (GObject *rd, GAsyncResult *res, gpointer data)
|
|
{
|
|
WpReserveDevice *self = WP_RESERVE_DEVICE (data);
|
|
g_autoptr (GError) error = NULL;
|
|
|
|
gboolean acquired = wp_reserve_device_acquire_transition_finish (res, &error);
|
|
if (error) {
|
|
wp_notice_object (self, "%s: Acquire error: %s", self->name,
|
|
error->message);
|
|
}
|
|
|
|
self->state = acquired ?
|
|
WP_RESERVE_DEVICE_STATE_ACQUIRED : WP_RESERVE_DEVICE_STATE_BUSY;
|
|
g_object_notify (G_OBJECT (self), "state");
|
|
update_owner_app_name (self);
|
|
}
|
|
|
|
static void
|
|
wp_reserve_device_acquire (WpReserveDevice * self)
|
|
{
|
|
g_autoptr (WpTransition) t = g_weak_ref_get (&self->transition);
|
|
if (self->state == WP_RESERVE_DEVICE_STATE_ACQUIRED ||
|
|
(t && !wp_transition_get_completed (t))) {
|
|
wp_debug_object (self, "%s: already acquired or operation in progress",
|
|
self->name);
|
|
return;
|
|
}
|
|
|
|
g_autoptr (WpReserveDevicePlugin) plugin = g_weak_ref_get (&self->plugin);
|
|
WpTransition *transition = wp_reserve_device_acquire_transition_new (self,
|
|
NULL, on_acquire_transition_done, self);
|
|
g_weak_ref_set (&self->transition, transition);
|
|
wp_transition_advance (transition);
|
|
}
|
|
|
|
static void
|
|
wp_reserve_device_release (WpReserveDevice * self)
|
|
{
|
|
if (self->state != WP_RESERVE_DEVICE_STATE_ACQUIRED) {
|
|
wp_debug_object (self, "%s: not acquired", self->name);
|
|
return;
|
|
}
|
|
|
|
/* set state to AVAILABLE to ensure that on_name_lost()
|
|
does not emit SIGNAL_REQUEST_RELEASE */
|
|
/* on_name_vanished() will emit the state change */
|
|
self->state = WP_RESERVE_DEVICE_STATE_AVAILABLE;
|
|
wp_reserve_device_unown_name (self);
|
|
|
|
if (self->req_rel_invocation) {
|
|
wp_org_freedesktop_reserve_device1_complete_request_release (NULL,
|
|
self->req_rel_invocation, TRUE);
|
|
self->req_rel_invocation = NULL;
|
|
}
|
|
}
|
|
|
|
static void
|
|
wp_reserve_device_deny_release (WpReserveDevice * self, gboolean success)
|
|
{
|
|
if (self->req_rel_invocation) {
|
|
wp_org_freedesktop_reserve_device1_complete_request_release (NULL,
|
|
self->req_rel_invocation, FALSE);
|
|
self->req_rel_invocation = NULL;
|
|
}
|
|
}
|
|
|
|
static gboolean
|
|
wp_reserve_device_handle_request_release (WpOrgFreedesktopReserveDevice1 *iface,
|
|
GDBusMethodInvocation *invocation, gint priority, gpointer user_data)
|
|
{
|
|
WpReserveDevice *self = WP_RESERVE_DEVICE (user_data);
|
|
|
|
/* deny release if the priority is lower than ours */
|
|
if (priority < self->priority) {
|
|
wp_org_freedesktop_reserve_device1_complete_request_release (iface,
|
|
g_object_ref (invocation), FALSE);
|
|
return TRUE;
|
|
}
|
|
|
|
/* else, request the release of the device from the implementation;
|
|
if signal handlers are connected, assume the functionality is implemented,
|
|
otherwise return FALSE to let the iface return UnknownMethod */
|
|
if (g_signal_has_handler_pending (self,
|
|
signals[SIGNAL_RELEASE_REQUESTED], 0, FALSE)) {
|
|
self->req_rel_invocation = g_object_ref (invocation);
|
|
g_signal_emit (self, signals[SIGNAL_RELEASE_REQUESTED], 0, FALSE);
|
|
return TRUE;
|
|
}
|
|
return FALSE;
|
|
}
|
|
|
|
static void
|
|
wp_reserve_device_get_property (GObject * object, guint property_id,
|
|
GValue * value, GParamSpec * pspec)
|
|
{
|
|
WpReserveDevice *self = WP_RESERVE_DEVICE (object);
|
|
|
|
switch (property_id) {
|
|
case PROP_NAME:
|
|
g_value_set_string (value, self->name);
|
|
break;
|
|
case PROP_APP_NAME:
|
|
g_value_set_string (value, self->app_name);
|
|
break;
|
|
case PROP_APP_DEV_NAME:
|
|
g_value_set_string (value, self->app_dev_name);
|
|
break;
|
|
case PROP_PRIORITY:
|
|
g_value_set_int (value, self->priority);
|
|
break;
|
|
case PROP_STATE:
|
|
g_value_set_enum (value, self->state);
|
|
break;
|
|
case PROP_OWNER_APP_NAME:
|
|
switch (self->state) {
|
|
case WP_RESERVE_DEVICE_STATE_ACQUIRED:
|
|
g_value_set_string (value, self->app_name);
|
|
break;
|
|
default:
|
|
g_value_set_string (value, self->owner_app_name);
|
|
break;
|
|
}
|
|
break;
|
|
default:
|
|
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void
|
|
wp_reserve_device_set_property (GObject * object,
|
|
guint property_id, const GValue * value, GParamSpec * pspec)
|
|
{
|
|
WpReserveDevice *self = WP_RESERVE_DEVICE (object);
|
|
|
|
switch (property_id) {
|
|
case PROP_PLUGIN:
|
|
g_weak_ref_set (&self->plugin, g_value_get_object (value));
|
|
break;
|
|
case PROP_NAME:
|
|
g_clear_pointer (&self->name, g_free);
|
|
self->name = g_value_dup_string (value);
|
|
break;
|
|
case PROP_APP_NAME:
|
|
g_clear_pointer (&self->app_name, g_free);
|
|
self->app_name = g_value_dup_string (value);
|
|
break;
|
|
case PROP_APP_DEV_NAME:
|
|
g_clear_pointer (&self->app_dev_name, g_free);
|
|
self->app_dev_name = g_value_dup_string (value);
|
|
break;
|
|
case PROP_PRIORITY:
|
|
self->priority = g_value_get_int(value);
|
|
break;
|
|
default:
|
|
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* wp_reserve_device_class_init
|
|
*
|
|
* @param klass: the reserve device class
|
|
*/
|
|
static void
|
|
wp_reserve_device_class_init (WpReserveDeviceClass * klass)
|
|
{
|
|
GObjectClass *object_class = (GObjectClass *) klass;
|
|
|
|
object_class->constructed = wp_reserve_device_constructed;
|
|
object_class->finalize = wp_reserve_device_finalize;
|
|
object_class->get_property = wp_reserve_device_get_property;
|
|
object_class->set_property = wp_reserve_device_set_property;
|
|
|
|
g_object_class_install_property (object_class, PROP_PLUGIN,
|
|
g_param_spec_object ("plugin", "plugin",
|
|
"The parent plugin instance", wp_reserve_device_plugin_get_type (),
|
|
G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
|
|
|
|
g_object_class_install_property (object_class, PROP_NAME,
|
|
g_param_spec_string ("name", "name",
|
|
"The reservation name", NULL,
|
|
G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
|
|
|
|
g_object_class_install_property (object_class, PROP_APP_NAME,
|
|
g_param_spec_string ("application-name", "application-name",
|
|
"The application name", NULL,
|
|
G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
|
|
|
|
g_object_class_install_property (object_class, PROP_APP_DEV_NAME,
|
|
g_param_spec_string ("application-device-name", "application-device-name",
|
|
"The application device name", NULL,
|
|
G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
|
|
|
|
g_object_class_install_property (object_class, PROP_PRIORITY,
|
|
g_param_spec_int ("priority", "priority",
|
|
"The priority", G_MININT, G_MAXINT, 0,
|
|
G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
|
|
|
|
g_object_class_install_property (object_class, PROP_STATE,
|
|
g_param_spec_enum ("state", "state", "The state",
|
|
WP_TYPE_RESERVE_DEVICE_STATE, WP_RESERVE_DEVICE_STATE_UNKNOWN,
|
|
G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
|
|
|
|
g_object_class_install_property (object_class, PROP_OWNER_APP_NAME,
|
|
g_param_spec_string ("owner-application-name", "owner-application-name",
|
|
"The owner application name", NULL,
|
|
G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
|
|
|
|
/**
|
|
* WpReserveDevice acquire:
|
|
*
|
|
* @section signal_acquire_section acquire
|
|
*/
|
|
signals[ACTION_ACQUIRE] = g_signal_new_class_handler (
|
|
"acquire", G_TYPE_FROM_CLASS (klass),
|
|
G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
|
|
(GCallback) wp_reserve_device_acquire,
|
|
NULL, NULL, NULL, G_TYPE_NONE, 0);
|
|
|
|
/**
|
|
* WpReserveDevice release:
|
|
*
|
|
* @section signal_release_section release
|
|
*/
|
|
signals[ACTION_RELEASE] = g_signal_new_class_handler (
|
|
"release", G_TYPE_FROM_CLASS (klass),
|
|
G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
|
|
(GCallback) wp_reserve_device_release,
|
|
NULL, NULL, NULL, G_TYPE_NONE, 0);
|
|
|
|
/**
|
|
* WpReserveDevice deny-release:
|
|
*
|
|
* @section signal_deny_release_section deny-release
|
|
*/
|
|
signals[ACTION_DENY_RELEASE] = g_signal_new_class_handler (
|
|
"deny-release", G_TYPE_FROM_CLASS (klass),
|
|
G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
|
|
(GCallback) wp_reserve_device_deny_release,
|
|
NULL, NULL, NULL, G_TYPE_NONE, 0);
|
|
|
|
/**
|
|
* WpReserveDevice release-requested:
|
|
*
|
|
* @section signal_release_requested_section release-requested
|
|
*
|
|
* @em forced: %TRUE if the name was forcibly taken from us,
|
|
* %FALSE if the `RequestRelease()` d-bus method was called
|
|
*
|
|
* @brief Signaled when the device needs to be released. If @em forced is %FALSE,
|
|
* call [release](@ref signal_release_section) to release or
|
|
* [deny-release](@ref signal_deny_release_section)
|
|
* to refuse and return %FALSE from the `RequestRelease()` d-bus method.
|
|
*/
|
|
signals[SIGNAL_RELEASE_REQUESTED] = g_signal_new (
|
|
"release-requested", G_TYPE_FROM_CLASS (klass),
|
|
G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL,
|
|
G_TYPE_NONE, 1, G_TYPE_BOOLEAN);
|
|
}
|
|
|
|
void
|
|
wp_reserve_device_export_object (WpReserveDevice *self)
|
|
{
|
|
g_autoptr (WpReserveDevicePlugin) plugin = g_weak_ref_get (&self->plugin);
|
|
if (plugin) {
|
|
g_autoptr (GDBusObjectSkeleton) skeleton =
|
|
g_dbus_object_skeleton_new (self->object_path);
|
|
g_autoptr (WpOrgFreedesktopReserveDevice1) iface =
|
|
wp_org_freedesktop_reserve_device1_skeleton_new ();
|
|
g_object_set (iface,
|
|
"priority", self->priority,
|
|
"application-name", self->app_name,
|
|
"application-device-name", self->app_dev_name,
|
|
NULL);
|
|
g_signal_connect_object (iface, "handle-request-release",
|
|
(GCallback) wp_reserve_device_handle_request_release, self, 0);
|
|
g_dbus_object_skeleton_add_interface (skeleton,
|
|
G_DBUS_INTERFACE_SKELETON (iface));
|
|
g_dbus_object_manager_server_export (plugin->manager, skeleton);
|
|
}
|
|
}
|
|
|
|
void
|
|
wp_reserve_device_unexport_object (WpReserveDevice *self)
|
|
{
|
|
g_autoptr (WpReserveDevicePlugin) plugin = g_weak_ref_get (&self->plugin);
|
|
if (plugin) {
|
|
wp_debug_object (self, "unexport %s", self->object_path);
|
|
g_dbus_object_manager_server_unexport (plugin->manager, self->object_path);
|
|
}
|
|
}
|
|
|
|
static void
|
|
on_name_acquired (GDBusConnection *connection, const gchar *name,
|
|
gpointer user_data)
|
|
{
|
|
WpReserveDevice *self = WP_RESERVE_DEVICE (user_data);
|
|
g_autoptr (WpTransition) t = g_weak_ref_get (&self->transition);
|
|
|
|
wp_debug_object (self, "%s acquired", name);
|
|
|
|
if (t)
|
|
wp_reserve_device_acquire_transition_name_acquired (t);
|
|
}
|
|
|
|
static void
|
|
on_name_lost (GDBusConnection *connection, const gchar *name,
|
|
gpointer user_data)
|
|
{
|
|
WpReserveDevice *self = WP_RESERVE_DEVICE (user_data);
|
|
g_autoptr (WpTransition) t = g_weak_ref_get (&self->transition);
|
|
|
|
wp_debug_object (self, "%s lost", name);
|
|
|
|
if (t) {
|
|
wp_reserve_device_acquire_transition_name_lost (t);
|
|
return;
|
|
}
|
|
|
|
if (self->state == WP_RESERVE_DEVICE_STATE_ACQUIRED) {
|
|
/* Emit release signal with forced set to TRUE */
|
|
g_signal_emit (self, signals[SIGNAL_RELEASE_REQUESTED], 0, TRUE);
|
|
wp_reserve_device_unown_name (self);
|
|
}
|
|
|
|
wp_reserve_device_unexport_object (self);
|
|
}
|
|
|
|
void
|
|
wp_reserve_device_own_name (WpReserveDevice * self, gboolean force)
|
|
{
|
|
g_return_if_fail (self->owner_id == 0);
|
|
|
|
g_autoptr (WpReserveDevicePlugin) plugin = g_weak_ref_get (&self->plugin);
|
|
if (plugin) {
|
|
g_autoptr (GDBusConnection) conn = NULL;
|
|
g_object_get (plugin->dbus, "connection", &conn, NULL);
|
|
|
|
GBusNameOwnerFlags flags = G_BUS_NAME_OWNER_FLAGS_DO_NOT_QUEUE;
|
|
if (self->priority != G_MAXINT32)
|
|
flags |= G_BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT;
|
|
if (force)
|
|
flags |= G_BUS_NAME_OWNER_FLAGS_REPLACE;
|
|
|
|
wp_debug_object (self, "request ownership of %s", self->service_name);
|
|
|
|
self->owner_id = g_bus_own_name_on_connection (conn,
|
|
self->service_name, flags, on_name_acquired, on_name_lost, self, NULL);
|
|
}
|
|
}
|
|
|
|
void
|
|
wp_reserve_device_unown_name (WpReserveDevice * self)
|
|
{
|
|
if (self->owner_id) {
|
|
wp_debug_object (self, "drop ownership of %s", self->service_name);
|
|
g_bus_unown_name (self->owner_id);
|
|
self->owner_id = 0;
|
|
}
|
|
}
|