glib/gio/tests/gmenumodel.c
Philip Withnall 5f5667b2a0
gmenuexporter: Fix a NULL pointer dereference on an error handling path
This latent bug wasn’t triggered until commit 3f30ec86c (or its
cherry-pick onto `glib-2-80`, 747e3af99, which was first released in
2.80.1).

That change means that `g_menu_exporter_free()` is now called on the
registration failure path by `g_dbus_connection_register_object()`
before it returns. The caller then tries to call `g_slice_free()` on the
exporter again. The call to `g_menu_exporter_free()` tries to
dereference/free members of the exporter which it expects to be
initialised — but because this is happening in an error handling path,
they are not initialised.

If it were to get any further, the `g_slice_free()` would then be a
double-free on the exporter allocation.

Fix that by making `g_menu_exporter_free()` robust to some of the
exporter members being `NULL`, and moving some of the initialisation
code higher in `g_dbus_connection_export_menu_model()`, and removing the
duplicate free code on the error handling path.

This includes a unit test.

Signed-off-by: Philip Withnall <pwithnall@gnome.org>

Fixes: #3366
2024-05-16 12:19:57 +01:00

1713 lines
46 KiB
C

#include <gio/gio.h>
#include <gio/gunixsocketaddress.h>
#include <glib/gstdio.h>
#include <string.h>
#include "gdbus-sessionbus.h"
#include "glib/glib-private.h"
static void
time_out (gpointer unused G_GNUC_UNUSED)
{
g_error ("Timed out");
}
static guint
add_timeout (guint seconds)
{
#ifdef G_OS_UNIX
/* Safety-catch against the main loop having blocked */
alarm (seconds + 5);
#endif
return g_timeout_add_seconds_once (seconds, time_out, NULL);
}
static void
cancel_timeout (guint timeout_id)
{
#ifdef G_OS_UNIX
alarm (0);
#endif
g_source_remove (timeout_id);
}
/* Markup printing {{{1 */
/* This used to be part of GLib, but it was removed before the stable
* release because it wasn't generally useful. We want it here, though.
*/
static void
indent_string (GString *string,
gint indent)
{
while (indent--)
g_string_append_c (string, ' ');
}
static GString *
g_menu_markup_print_string (GString *string,
GMenuModel *model,
gint indent,
gint tabstop)
{
gboolean need_nl = FALSE;
gint i, n;
if G_UNLIKELY (string == NULL)
string = g_string_new (NULL);
n = g_menu_model_get_n_items (model);
for (i = 0; i < n; i++)
{
GMenuAttributeIter *attr_iter;
GMenuLinkIter *link_iter;
GString *contents;
GString *attrs;
attr_iter = g_menu_model_iterate_item_attributes (model, i);
link_iter = g_menu_model_iterate_item_links (model, i);
contents = g_string_new (NULL);
attrs = g_string_new (NULL);
while (g_menu_attribute_iter_next (attr_iter))
{
const char *name = g_menu_attribute_iter_get_name (attr_iter);
GVariant *value = g_menu_attribute_iter_get_value (attr_iter);
if (g_variant_is_of_type (value, G_VARIANT_TYPE_STRING))
{
gchar *str;
str = g_markup_printf_escaped (" %s='%s'", name, g_variant_get_string (value, NULL));
g_string_append (attrs, str);
g_free (str);
}
else
{
gchar *printed;
gchar *str;
const gchar *type;
printed = g_variant_print (value, TRUE);
type = g_variant_type_peek_string (g_variant_get_type (value));
str = g_markup_printf_escaped ("<attribute name='%s' type='%s'>%s</attribute>\n", name, type, printed);
indent_string (contents, indent + tabstop);
g_string_append (contents, str);
g_free (printed);
g_free (str);
}
g_variant_unref (value);
}
g_object_unref (attr_iter);
while (g_menu_link_iter_next (link_iter))
{
const gchar *name = g_menu_link_iter_get_name (link_iter);
GMenuModel *menu = g_menu_link_iter_get_value (link_iter);
gchar *str;
if (contents->str[0])
g_string_append_c (contents, '\n');
str = g_markup_printf_escaped ("<link name='%s'>\n", name);
indent_string (contents, indent + tabstop);
g_string_append (contents, str);
g_free (str);
g_menu_markup_print_string (contents, menu, indent + 2 * tabstop, tabstop);
indent_string (contents, indent + tabstop);
g_string_append (contents, "</link>\n");
g_object_unref (menu);
}
g_object_unref (link_iter);
if (contents->str[0])
{
indent_string (string, indent);
g_string_append_printf (string, "<item%s>\n", attrs->str);
g_string_append (string, contents->str);
indent_string (string, indent);
g_string_append (string, "</item>\n");
need_nl = TRUE;
}
else
{
if (need_nl)
g_string_append_c (string, '\n');
indent_string (string, indent);
g_string_append_printf (string, "<item%s/>\n", attrs->str);
need_nl = FALSE;
}
g_string_free (contents, TRUE);
g_string_free (attrs, TRUE);
}
return string;
}
/* TestItem {{{1 */
/* This utility struct is used by both the RandomMenu and MirrorMenu
* class implementations below.
*/
typedef struct {
GHashTable *attributes;
GHashTable *links;
} TestItem;
static TestItem *
test_item_new (GHashTable *attributes,
GHashTable *links)
{
TestItem *item;
item = g_slice_new (TestItem);
item->attributes = g_hash_table_ref (attributes);
item->links = g_hash_table_ref (links);
return item;
}
static void
test_item_free (gpointer data)
{
TestItem *item = data;
g_hash_table_unref (item->attributes);
g_hash_table_unref (item->links);
g_slice_free (TestItem, item);
}
/* RandomMenu {{{1 */
#define MAX_ITEMS 5
#define TOP_ORDER 4
typedef struct {
GMenuModel parent_instance;
GSequence *items;
gint order;
} RandomMenu;
typedef GMenuModelClass RandomMenuClass;
static GType random_menu_get_type (void);
G_DEFINE_TYPE (RandomMenu, random_menu, G_TYPE_MENU_MODEL)
static gboolean
random_menu_is_mutable (GMenuModel *model)
{
return TRUE;
}
static gint
random_menu_get_n_items (GMenuModel *model)
{
RandomMenu *menu = (RandomMenu *) model;
return g_sequence_get_length (menu->items);
}
static void
random_menu_get_item_attributes (GMenuModel *model,
gint position,
GHashTable **table)
{
RandomMenu *menu = (RandomMenu *) model;
TestItem *item;
item = g_sequence_get (g_sequence_get_iter_at_pos (menu->items, position));
*table = g_hash_table_ref (item->attributes);
}
static void
random_menu_get_item_links (GMenuModel *model,
gint position,
GHashTable **table)
{
RandomMenu *menu = (RandomMenu *) model;
TestItem *item;
item = g_sequence_get (g_sequence_get_iter_at_pos (menu->items, position));
*table = g_hash_table_ref (item->links);
}
static void
random_menu_finalize (GObject *object)
{
RandomMenu *menu = (RandomMenu *) object;
g_sequence_free (menu->items);
G_OBJECT_CLASS (random_menu_parent_class)
->finalize (object);
}
static void
random_menu_init (RandomMenu *menu)
{
}
static void
random_menu_class_init (GMenuModelClass *class)
{
GObjectClass *object_class = G_OBJECT_CLASS (class);
class->is_mutable = random_menu_is_mutable;
class->get_n_items = random_menu_get_n_items;
class->get_item_attributes = random_menu_get_item_attributes;
class->get_item_links = random_menu_get_item_links;
object_class->finalize = random_menu_finalize;
}
static RandomMenu * random_menu_new (GRand *rand, gint order);
static void
random_menu_change (RandomMenu *menu,
GRand *rand)
{
gint position, removes, adds;
GSequenceIter *point;
gint n_items;
gint i;
n_items = g_sequence_get_length (menu->items);
do
{
position = g_rand_int_range (rand, 0, n_items + 1);
removes = g_rand_int_range (rand, 0, n_items - position + 1);
adds = g_rand_int_range (rand, 0, MAX_ITEMS - (n_items - removes) + 1);
}
while (removes == 0 && adds == 0);
point = g_sequence_get_iter_at_pos (menu->items, position + removes);
if (removes)
{
GSequenceIter *start;
start = g_sequence_get_iter_at_pos (menu->items, position);
g_sequence_remove_range (start, point);
}
for (i = 0; i < adds; i++)
{
const gchar *label;
GHashTable *links;
GHashTable *attributes;
attributes = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify) g_variant_unref);
links = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify) g_object_unref);
if (menu->order > 0 && g_rand_boolean (rand))
{
RandomMenu *child;
const gchar *subtype;
child = random_menu_new (rand, menu->order - 1);
if (g_rand_boolean (rand))
{
subtype = G_MENU_LINK_SECTION;
/* label some section headers */
if (g_rand_boolean (rand))
label = "Section";
else
label = NULL;
}
else
{
/* label all submenus */
subtype = G_MENU_LINK_SUBMENU;
label = "Submenu";
}
g_hash_table_insert (links, g_strdup (subtype), child);
}
else
/* label all terminals */
label = "Menu Item";
if (label)
g_hash_table_insert (attributes, g_strdup ("label"), g_variant_ref_sink (g_variant_new_string (label)));
g_sequence_insert_before (point, test_item_new (attributes, links));
g_hash_table_unref (links);
g_hash_table_unref (attributes);
}
g_menu_model_items_changed (G_MENU_MODEL (menu), position, removes, adds);
}
static RandomMenu *
random_menu_new (GRand *rand,
gint order)
{
RandomMenu *menu;
menu = g_object_new (random_menu_get_type (), NULL);
menu->items = g_sequence_new (test_item_free);
menu->order = order;
random_menu_change (menu, rand);
return menu;
}
/* MirrorMenu {{{1 */
typedef struct {
GMenuModel parent_instance;
GMenuModel *clone_of;
GSequence *items;
gulong handler_id;
} MirrorMenu;
typedef GMenuModelClass MirrorMenuClass;
static GType mirror_menu_get_type (void);
G_DEFINE_TYPE (MirrorMenu, mirror_menu, G_TYPE_MENU_MODEL)
static gboolean
mirror_menu_is_mutable (GMenuModel *model)
{
MirrorMenu *menu = (MirrorMenu *) model;
return menu->handler_id != 0;
}
static gint
mirror_menu_get_n_items (GMenuModel *model)
{
MirrorMenu *menu = (MirrorMenu *) model;
return g_sequence_get_length (menu->items);
}
static void
mirror_menu_get_item_attributes (GMenuModel *model,
gint position,
GHashTable **table)
{
MirrorMenu *menu = (MirrorMenu *) model;
TestItem *item;
item = g_sequence_get (g_sequence_get_iter_at_pos (menu->items, position));
*table = g_hash_table_ref (item->attributes);
}
static void
mirror_menu_get_item_links (GMenuModel *model,
gint position,
GHashTable **table)
{
MirrorMenu *menu = (MirrorMenu *) model;
TestItem *item;
item = g_sequence_get (g_sequence_get_iter_at_pos (menu->items, position));
*table = g_hash_table_ref (item->links);
}
static void
mirror_menu_finalize (GObject *object)
{
MirrorMenu *menu = (MirrorMenu *) object;
if (menu->handler_id)
g_signal_handler_disconnect (menu->clone_of, menu->handler_id);
g_sequence_free (menu->items);
g_object_unref (menu->clone_of);
G_OBJECT_CLASS (mirror_menu_parent_class)
->finalize (object);
}
static void
mirror_menu_init (MirrorMenu *menu)
{
}
static void
mirror_menu_class_init (GMenuModelClass *class)
{
GObjectClass *object_class = G_OBJECT_CLASS (class);
class->is_mutable = mirror_menu_is_mutable;
class->get_n_items = mirror_menu_get_n_items;
class->get_item_attributes = mirror_menu_get_item_attributes;
class->get_item_links = mirror_menu_get_item_links;
object_class->finalize = mirror_menu_finalize;
}
static MirrorMenu * mirror_menu_new (GMenuModel *clone_of);
static void
mirror_menu_changed (GMenuModel *model,
gint position,
gint removed,
gint added,
gpointer user_data)
{
MirrorMenu *menu = user_data;
GSequenceIter *point;
gint i;
g_assert (model == menu->clone_of);
point = g_sequence_get_iter_at_pos (menu->items, position + removed);
if (removed)
{
GSequenceIter *start;
start = g_sequence_get_iter_at_pos (menu->items, position);
g_sequence_remove_range (start, point);
}
for (i = position; i < position + added; i++)
{
GMenuAttributeIter *attr_iter;
GMenuLinkIter *link_iter;
GHashTable *links;
GHashTable *attributes;
const gchar *name;
GMenuModel *child;
GVariant *value;
attributes = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify) g_variant_unref);
links = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify) g_object_unref);
attr_iter = g_menu_model_iterate_item_attributes (model, i);
while (g_menu_attribute_iter_get_next (attr_iter, &name, &value))
{
g_hash_table_insert (attributes, g_strdup (name), value);
}
g_object_unref (attr_iter);
link_iter = g_menu_model_iterate_item_links (model, i);
while (g_menu_link_iter_get_next (link_iter, &name, &child))
{
g_hash_table_insert (links, g_strdup (name), mirror_menu_new (child));
g_object_unref (child);
}
g_object_unref (link_iter);
g_sequence_insert_before (point, test_item_new (attributes, links));
g_hash_table_unref (attributes);
g_hash_table_unref (links);
}
g_menu_model_items_changed (G_MENU_MODEL (menu), position, removed, added);
}
static MirrorMenu *
mirror_menu_new (GMenuModel *clone_of)
{
MirrorMenu *menu;
menu = g_object_new (mirror_menu_get_type (), NULL);
menu->items = g_sequence_new (test_item_free);
menu->clone_of = g_object_ref (clone_of);
if (g_menu_model_is_mutable (clone_of))
menu->handler_id = g_signal_connect (clone_of, "items-changed", G_CALLBACK (mirror_menu_changed), menu);
mirror_menu_changed (clone_of, 0, 0, g_menu_model_get_n_items (clone_of), menu);
return menu;
}
/* check_menus_equal(), assert_menus_equal() {{{1 */
static gboolean
check_menus_equal (GMenuModel *a,
GMenuModel *b)
{
gboolean equal = TRUE;
gint a_n, b_n;
gint i;
a_n = g_menu_model_get_n_items (a);
b_n = g_menu_model_get_n_items (b);
if (a_n != b_n)
return FALSE;
for (i = 0; i < a_n; i++)
{
GMenuAttributeIter *attr_iter;
GVariant *a_value, *b_value;
GMenuLinkIter *link_iter;
GMenuModel *a_menu, *b_menu;
const gchar *name;
attr_iter = g_menu_model_iterate_item_attributes (a, i);
while (g_menu_attribute_iter_get_next (attr_iter, &name, &a_value))
{
b_value = g_menu_model_get_item_attribute_value (b, i, name, NULL);
equal &= b_value && g_variant_equal (a_value, b_value);
if (b_value)
g_variant_unref (b_value);
g_variant_unref (a_value);
}
g_object_unref (attr_iter);
attr_iter = g_menu_model_iterate_item_attributes (b, i);
while (g_menu_attribute_iter_get_next (attr_iter, &name, &b_value))
{
a_value = g_menu_model_get_item_attribute_value (a, i, name, NULL);
equal &= a_value && g_variant_equal (a_value, b_value);
if (a_value)
g_variant_unref (a_value);
g_variant_unref (b_value);
}
g_object_unref (attr_iter);
link_iter = g_menu_model_iterate_item_links (a, i);
while (g_menu_link_iter_get_next (link_iter, &name, &a_menu))
{
b_menu = g_menu_model_get_item_link (b, i, name);
equal &= b_menu && check_menus_equal (a_menu, b_menu);
if (b_menu)
g_object_unref (b_menu);
g_object_unref (a_menu);
}
g_object_unref (link_iter);
link_iter = g_menu_model_iterate_item_links (b, i);
while (g_menu_link_iter_get_next (link_iter, &name, &b_menu))
{
a_menu = g_menu_model_get_item_link (a, i, name);
equal &= a_menu && check_menus_equal (a_menu, b_menu);
if (a_menu)
g_object_unref (a_menu);
g_object_unref (b_menu);
}
g_object_unref (link_iter);
}
return equal;
}
static void
assert_menus_equal (GMenuModel *a,
GMenuModel *b)
{
if (!check_menus_equal (a, b))
{
GString *string;
string = g_string_new ("\n <a>\n");
g_menu_markup_print_string (string, G_MENU_MODEL (a), 4, 2);
g_string_append (string, " </a>\n\n-------------\n <b>\n");
g_menu_markup_print_string (string, G_MENU_MODEL (b), 4, 2);
g_string_append (string, " </b>\n");
g_error ("%s", string->str);
}
}
static void
assert_menuitem_equal (GMenuItem *item,
GMenuModel *model,
gint index)
{
GMenuAttributeIter *attr_iter;
GMenuLinkIter *link_iter;
const gchar *name;
GVariant *value;
GMenuModel *linked_model;
/* NOTE we can't yet test whether item has attributes or links that
* are not in the model, because there's no iterator API for menu
* items */
attr_iter = g_menu_model_iterate_item_attributes (model, index);
while (g_menu_attribute_iter_get_next (attr_iter, &name, &value))
{
GVariant *item_value;
item_value = g_menu_item_get_attribute_value (item, name, g_variant_get_type (value));
g_assert (item_value && g_variant_equal (item_value, value));
g_variant_unref (item_value);
g_variant_unref (value);
}
link_iter = g_menu_model_iterate_item_links (model, index);
while (g_menu_link_iter_get_next (link_iter, &name, &linked_model))
{
GMenuModel *item_linked_model;
item_linked_model = g_menu_item_get_link (item, name);
g_assert (linked_model == item_linked_model);
g_object_unref (item_linked_model);
g_object_unref (linked_model);
}
g_object_unref (attr_iter);
g_object_unref (link_iter);
}
/* Test cases {{{1 */
static void
test_equality (void)
{
GRand *randa, *randb;
guint32 seed;
gint i;
seed = g_test_rand_int ();
randa = g_rand_new_with_seed (seed);
randb = g_rand_new_with_seed (seed);
for (i = 0; i < 500; i++)
{
RandomMenu *a, *b;
a = random_menu_new (randa, TOP_ORDER);
b = random_menu_new (randb, TOP_ORDER);
assert_menus_equal (G_MENU_MODEL (a), G_MENU_MODEL (b));
g_object_unref (b);
g_object_unref (a);
}
g_rand_int (randa);
for (i = 0; i < 500;)
{
RandomMenu *a, *b;
a = random_menu_new (randa, TOP_ORDER);
b = random_menu_new (randb, TOP_ORDER);
if (check_menus_equal (G_MENU_MODEL (a), G_MENU_MODEL (b)))
{
/* by chance, they may really be equal. double check. */
GString *as, *bs;
as = g_menu_markup_print_string (NULL, G_MENU_MODEL (a), 4, 2);
bs = g_menu_markup_print_string (NULL, G_MENU_MODEL (b), 4, 2);
g_assert_cmpstr (as->str, ==, bs->str);
g_string_free (bs, TRUE);
g_string_free (as, TRUE);
/* we're here because randa and randb just generated equal
* menus. they may do it again, so throw away randb and make
* a fresh one.
*/
g_rand_free (randb);
randb = g_rand_new_with_seed (g_rand_int (randa));
}
else
/* make sure we get enough unequals (ie: no GRand failure) */
i++;
g_object_unref (b);
g_object_unref (a);
}
g_rand_free (randb);
g_rand_free (randa);
}
static void
test_random (void)
{
RandomMenu *random;
MirrorMenu *mirror;
GRand *rand;
gint i;
rand = g_rand_new_with_seed (g_test_rand_int ());
random = random_menu_new (rand, TOP_ORDER);
mirror = mirror_menu_new (G_MENU_MODEL (random));
for (i = 0; i < 500; i++)
{
assert_menus_equal (G_MENU_MODEL (random), G_MENU_MODEL (mirror));
random_menu_change (random, rand);
}
g_object_unref (mirror);
g_object_unref (random);
g_rand_free (rand);
}
typedef struct
{
GDBusConnection *client_connection;
GDBusConnection *server_connection;
GDBusServer *server;
GThread *service_thread;
/* Protects server_connection and service_loop. */
GMutex service_loop_lock;
GCond service_loop_cond;
GMainLoop *service_loop;
} PeerConnection;
static gboolean
on_new_connection (GDBusServer *server,
GDBusConnection *connection,
gpointer user_data)
{
PeerConnection *data = user_data;
g_mutex_lock (&data->service_loop_lock);
data->server_connection = g_object_ref (connection);
g_cond_broadcast (&data->service_loop_cond);
g_mutex_unlock (&data->service_loop_lock);
return TRUE;
}
static void
create_service_loop (GMainContext *service_context,
PeerConnection *data)
{
g_assert (data->service_loop == NULL);
g_mutex_lock (&data->service_loop_lock);
data->service_loop = g_main_loop_new (service_context, FALSE);
g_cond_broadcast (&data->service_loop_cond);
g_mutex_unlock (&data->service_loop_lock);
}
static void
teardown_service_loop (PeerConnection *data)
{
g_mutex_lock (&data->service_loop_lock);
g_clear_pointer (&data->service_loop, g_main_loop_unref);
g_mutex_unlock (&data->service_loop_lock);
}
static void
await_service_loop (PeerConnection *data)
{
g_mutex_lock (&data->service_loop_lock);
while (data->service_loop == NULL)
g_cond_wait (&data->service_loop_cond, &data->service_loop_lock);
g_mutex_unlock (&data->service_loop_lock);
}
static void
await_server_connection (PeerConnection *data)
{
g_mutex_lock (&data->service_loop_lock);
while (data->server_connection == NULL)
g_cond_wait (&data->service_loop_cond, &data->service_loop_lock);
g_mutex_unlock (&data->service_loop_lock);
}
static gpointer
service_thread_func (gpointer user_data)
{
PeerConnection *data = user_data;
GMainContext *service_context;
GError *error;
gchar *address;
gchar *tmpdir;
GDBusServerFlags flags;
gchar *guid;
service_context = g_main_context_new ();
g_main_context_push_thread_default (service_context);
tmpdir = NULL;
flags = G_DBUS_SERVER_FLAGS_NONE;
#ifdef G_OS_UNIX
tmpdir = g_dir_make_tmp ("test-dbus-peer-XXXXXX", NULL);
address = g_strdup_printf ("unix:tmpdir=%s", tmpdir);
#else
address = g_strdup ("nonce-tcp:");
flags |= G_DBUS_SERVER_FLAGS_AUTHENTICATION_ALLOW_ANONYMOUS;
#endif
guid = g_dbus_generate_guid ();
error = NULL;
data->server = g_dbus_server_new_sync (address,
flags,
guid,
NULL,
NULL,
&error);
g_assert_no_error (error);
g_free (address);
g_free (guid);
g_signal_connect (data->server,
"new-connection",
G_CALLBACK (on_new_connection),
data);
g_dbus_server_start (data->server);
create_service_loop (service_context, data);
g_main_loop_run (data->service_loop);
g_main_context_pop_thread_default (service_context);
teardown_service_loop (data);
g_main_context_unref (service_context);
if (tmpdir)
{
g_rmdir (tmpdir);
g_free (tmpdir);
}
return NULL;
}
static void
peer_connection_up (PeerConnection *data)
{
GError *error;
memset (data, '\0', sizeof (PeerConnection));
g_mutex_init (&data->service_loop_lock);
g_cond_init (&data->service_loop_cond);
/* bring up a server - we run the server in a different thread to
avoid deadlocks */
data->service_thread = g_thread_new ("test_dbus_peer",
service_thread_func,
data);
await_service_loop (data);
g_assert (data->server != NULL);
/* bring up a connection and accept it */
error = NULL;
data->client_connection =
g_dbus_connection_new_for_address_sync (g_dbus_server_get_client_address (data->server),
G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT,
NULL, /* GDBusAuthObserver */
NULL, /* cancellable */
&error);
g_assert_no_error (error);
g_assert (data->client_connection != NULL);
await_server_connection (data);
}
static void
peer_connection_down (PeerConnection *data)
{
g_object_unref (data->client_connection);
g_object_unref (data->server_connection);
g_dbus_server_stop (data->server);
g_object_unref (data->server);
g_main_loop_quit (data->service_loop);
g_thread_join (data->service_thread);
g_mutex_clear (&data->service_loop_lock);
g_cond_clear (&data->service_loop_cond);
}
struct roundtrip_state
{
RandomMenu *random;
MirrorMenu *proxy_mirror;
GDBusMenuModel *proxy;
GMainLoop *loop;
GRand *rand;
gint success;
gint count;
};
static gboolean
roundtrip_step (gpointer data)
{
struct roundtrip_state *state = data;
if (check_menus_equal (G_MENU_MODEL (state->random), G_MENU_MODEL (state->proxy)) &&
check_menus_equal (G_MENU_MODEL (state->random), G_MENU_MODEL (state->proxy_mirror)))
{
state->success++;
state->count = 0;
if (state->success < 100)
random_menu_change (state->random, state->rand);
else
g_main_loop_quit (state->loop);
}
else if (state->count == 100)
{
assert_menus_equal (G_MENU_MODEL (state->random), G_MENU_MODEL (state->proxy));
g_assert_not_reached ();
}
else
state->count++;
return G_SOURCE_CONTINUE;
}
static void
do_roundtrip (GDBusConnection *exporter_connection,
GDBusConnection *proxy_connection)
{
struct roundtrip_state state;
guint export_id;
guint id;
state.rand = g_rand_new_with_seed (g_test_rand_int ());
state.random = random_menu_new (state.rand, 2);
export_id = g_dbus_connection_export_menu_model (exporter_connection,
"/",
G_MENU_MODEL (state.random),
NULL);
state.proxy = g_dbus_menu_model_get (proxy_connection,
g_dbus_connection_get_unique_name (proxy_connection),
"/");
state.proxy_mirror = mirror_menu_new (G_MENU_MODEL (state.proxy));
state.count = 0;
state.success = 0;
id = g_timeout_add (10, roundtrip_step, &state);
state.loop = g_main_loop_new (NULL, FALSE);
g_main_loop_run (state.loop);
g_main_loop_unref (state.loop);
g_source_remove (id);
g_object_unref (state.proxy);
g_dbus_connection_unexport_menu_model (exporter_connection, export_id);
g_object_unref (state.random);
g_object_unref (state.proxy_mirror);
g_rand_free (state.rand);
}
static void
test_dbus_roundtrip (void)
{
GDBusConnection *bus;
bus = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, NULL);
do_roundtrip (bus, bus);
g_object_unref (bus);
}
static void
test_dbus_peer_roundtrip (void)
{
#ifdef _GLIB_ADDRESS_SANITIZER
g_test_incomplete ("FIXME: Leaks a GCancellableSource, see glib#2313");
(void) peer_connection_up;
(void) peer_connection_down;
#else
PeerConnection peer;
peer_connection_up (&peer);
do_roundtrip (peer.server_connection, peer.client_connection);
peer_connection_down (&peer);
#endif
}
static gint items_changed_count;
static void
items_changed (GMenuModel *model,
gint position,
gint removed,
gint added,
gpointer data)
{
items_changed_count++;
}
static gboolean
stop_loop (gpointer data)
{
GMainLoop *loop = data;
g_main_loop_quit (loop);
return G_SOURCE_REMOVE;
}
static void
do_subscriptions (GDBusConnection *exporter_connection,
GDBusConnection *proxy_connection)
{
GMenu *menu;
GDBusMenuModel *proxy;
GMainLoop *loop;
GError *error = NULL;
guint export_id;
guint timeout_id;
timeout_id = add_timeout (60);
loop = g_main_loop_new (NULL, FALSE);
menu = g_menu_new ();
export_id = g_dbus_connection_export_menu_model (exporter_connection,
"/",
G_MENU_MODEL (menu),
&error);
g_assert_no_error (error);
proxy = g_dbus_menu_model_get (proxy_connection,
g_dbus_connection_get_unique_name (proxy_connection),
"/");
items_changed_count = 0;
g_signal_connect (proxy, "items-changed",
G_CALLBACK (items_changed), NULL);
g_menu_append (menu, "item1", NULL);
g_menu_append (menu, "item2", NULL);
g_menu_append (menu, "item3", NULL);
g_assert_cmpint (items_changed_count, ==, 0);
/* We don't subscribe to change-notification until we look at the items */
g_timeout_add (100, stop_loop, loop);
g_main_loop_run (loop);
/* Looking at the items triggers subscription */
g_menu_model_get_n_items (G_MENU_MODEL (proxy));
while (items_changed_count < 1)
g_main_context_iteration (NULL, TRUE);
/* We get all three items in one batch */
g_assert_cmpint (items_changed_count, ==, 1);
g_assert_cmpint (g_menu_model_get_n_items (G_MENU_MODEL (proxy)), ==, 3);
/* If we wait, we don't get any more */
g_timeout_add (100, stop_loop, loop);
g_main_loop_run (loop);
g_assert_cmpint (items_changed_count, ==, 1);
g_assert_cmpint (g_menu_model_get_n_items (G_MENU_MODEL (proxy)), ==, 3);
/* Now we're subscribed, we get changes individually */
g_menu_append (menu, "item4", NULL);
g_menu_append (menu, "item5", NULL);
g_menu_append (menu, "item6", NULL);
g_menu_remove (menu, 0);
g_menu_remove (menu, 0);
while (items_changed_count < 6)
g_main_context_iteration (NULL, TRUE);
g_assert_cmpint (items_changed_count, ==, 6);
g_assert_cmpint (g_menu_model_get_n_items (G_MENU_MODEL (proxy)), ==, 4);
/* After destroying the proxy and waiting a bit, we don't get any more
* items-changed signals */
g_object_unref (proxy);
g_timeout_add (100, stop_loop, loop);
g_main_loop_run (loop);
g_menu_remove (menu, 0);
g_menu_remove (menu, 0);
g_timeout_add (100, stop_loop, loop);
g_main_loop_run (loop);
g_assert_cmpint (items_changed_count, ==, 6);
g_dbus_connection_unexport_menu_model (exporter_connection, export_id);
g_object_unref (menu);
g_main_loop_unref (loop);
cancel_timeout (timeout_id);
}
static void
test_dbus_subscriptions (void)
{
GDBusConnection *bus;
bus = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, NULL);
do_subscriptions (bus, bus);
g_object_unref (bus);
}
static void
test_dbus_peer_subscriptions (void)
{
#ifdef _GLIB_ADDRESS_SANITIZER
g_test_incomplete ("FIXME: Leaks a GCancellableSource, see glib#2313");
(void) peer_connection_up;
(void) peer_connection_down;
#else
PeerConnection peer;
peer_connection_up (&peer);
do_subscriptions (peer.server_connection, peer.client_connection);
peer_connection_down (&peer);
#endif
}
static void
test_dbus_export_error_handling (void)
{
GRand *rand = NULL;
RandomMenu *menu = NULL;
GDBusConnection *bus;
GError *local_error = NULL;
guint id1, id2;
g_test_summary ("Test that error handling of menu model export failure works");
g_test_bug ("https://gitlab.gnome.org/GNOME/glib/-/issues/3366");
bus = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, NULL);
rand = g_rand_new_with_seed (g_test_rand_int ());
menu = random_menu_new (rand, 2);
id1 = g_dbus_connection_export_menu_model (bus, "/", G_MENU_MODEL (menu), &local_error);
g_assert_no_error (local_error);
g_assert_cmpuint (id1, !=, 0);
/* Trigger a failure by trying to export on a path which is already in use */
id2 = g_dbus_connection_export_menu_model (bus, "/", G_MENU_MODEL (menu), &local_error);
g_assert_error (local_error, G_IO_ERROR, G_IO_ERROR_EXISTS);
g_assert_cmpuint (id2, ==, 0);
g_clear_error (&local_error);
g_dbus_connection_unexport_menu_model (bus, id1);
while (g_main_context_iteration (NULL, FALSE));
g_clear_object (&menu);
g_rand_free (rand);
g_clear_object (&bus);
}
static gpointer
do_modify (gpointer data)
{
RandomMenu *menu = data;
GRand *rand;
gint i;
rand = g_rand_new_with_seed (g_test_rand_int ());
for (i = 0; i < 10000; i++)
{
random_menu_change (menu, rand);
}
g_rand_free (rand);
return NULL;
}
static gpointer
do_export (gpointer data)
{
GMenuModel *menu = data;
gint i;
GDBusConnection *bus;
gchar *path;
GError *error = NULL;
guint id;
bus = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, NULL);
path = g_strdup_printf ("/%p", data);
for (i = 0; i < 10000; i++)
{
id = g_dbus_connection_export_menu_model (bus, path, menu, &error);
g_assert_no_error (error);
g_dbus_connection_unexport_menu_model (bus, id);
while (g_main_context_iteration (NULL, FALSE));
}
g_free (path);
g_object_unref (bus);
return NULL;
}
static void
test_dbus_threaded (void)
{
RandomMenu *menu[10];
GThread *call[10];
GThread *export[10];
gint i;
for (i = 0; i < 10; i++)
{
GRand *rand = g_rand_new_with_seed (g_test_rand_int ());
menu[i] = random_menu_new (rand, 2);
call[i] = g_thread_new ("call", do_modify, menu[i]);
export[i] = g_thread_new ("export", do_export, menu[i]);
g_rand_free (rand);
}
for (i = 0; i < 10; i++)
{
g_thread_join (call[i]);
g_thread_join (export[i]);
}
for (i = 0; i < 10; i++)
g_object_unref (menu[i]);
}
static void
test_attributes (void)
{
GMenu *menu;
GMenuItem *item;
GVariant *v;
menu = g_menu_new ();
item = g_menu_item_new ("test", NULL);
g_menu_item_set_attribute_value (item, "boolean", g_variant_new_boolean (FALSE));
g_menu_item_set_attribute_value (item, "string", g_variant_new_string ("bla"));
g_menu_item_set_attribute (item, "double", "d", 1.5);
v = g_variant_new_parsed ("[('one', 1), ('two', %i), (%s, 3)]", 2, "three");
g_menu_item_set_attribute_value (item, "complex", v);
g_menu_item_set_attribute_value (item, "test-123", g_variant_new_string ("test-123"));
g_menu_append_item (menu, item);
g_menu_item_set_attribute (item, "double", "d", G_PI);
g_assert_cmpint (g_menu_model_get_n_items (G_MENU_MODEL (menu)), ==, 1);
v = g_menu_model_get_item_attribute_value (G_MENU_MODEL (menu), 0, "boolean", NULL);
g_assert (g_variant_is_of_type (v, G_VARIANT_TYPE_BOOLEAN));
g_variant_unref (v);
v = g_menu_model_get_item_attribute_value (G_MENU_MODEL (menu), 0, "string", NULL);
g_assert (g_variant_is_of_type (v, G_VARIANT_TYPE_STRING));
g_variant_unref (v);
v = g_menu_model_get_item_attribute_value (G_MENU_MODEL (menu), 0, "double", NULL);
g_assert (g_variant_is_of_type (v, G_VARIANT_TYPE_DOUBLE));
g_variant_unref (v);
v = g_menu_model_get_item_attribute_value (G_MENU_MODEL (menu), 0, "complex", NULL);
g_assert (g_variant_is_of_type (v, G_VARIANT_TYPE("a(si)")));
g_variant_unref (v);
g_menu_remove_all (menu);
g_object_unref (menu);
g_object_unref (item);
}
static void
test_attribute_iter (void)
{
GMenu *menu;
GMenuItem *item;
const gchar *name;
GVariant *v;
GMenuAttributeIter *iter;
GHashTable *found;
menu = g_menu_new ();
item = g_menu_item_new ("test", NULL);
g_menu_item_set_attribute_value (item, "boolean", g_variant_new_boolean (FALSE));
g_menu_item_set_attribute_value (item, "string", g_variant_new_string ("bla"));
g_menu_item_set_attribute (item, "double", "d", 1.5);
v = g_variant_new_parsed ("[('one', 1), ('two', %i), (%s, 3)]", 2, "three");
g_menu_item_set_attribute_value (item, "complex", v);
g_menu_item_set_attribute_value (item, "test-123", g_variant_new_string ("test-123"));
g_menu_append_item (menu, item);
g_menu_item_set_attribute (item, "double", "d", G_PI);
g_assert_cmpint (g_menu_model_get_n_items (G_MENU_MODEL (menu)), ==, 1);
found = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify)g_variant_unref);
iter = g_menu_model_iterate_item_attributes (G_MENU_MODEL (menu), 0);
while (g_menu_attribute_iter_get_next (iter, &name, &v))
g_hash_table_insert (found, g_strdup (name), v);
g_object_unref (iter);
g_assert_cmpint (g_hash_table_size (found), ==, 6);
v = g_hash_table_lookup (found, "label");
g_assert (g_variant_is_of_type (v, G_VARIANT_TYPE_STRING));
v = g_hash_table_lookup (found, "boolean");
g_assert (g_variant_is_of_type (v, G_VARIANT_TYPE_BOOLEAN));
v = g_hash_table_lookup (found, "string");
g_assert (g_variant_is_of_type (v, G_VARIANT_TYPE_STRING));
v = g_hash_table_lookup (found, "double");
g_assert (g_variant_is_of_type (v, G_VARIANT_TYPE_DOUBLE));
v = g_hash_table_lookup (found, "complex");
g_assert (g_variant_is_of_type (v, G_VARIANT_TYPE("a(si)")));
v = g_hash_table_lookup (found, "test-123");
g_assert (g_variant_is_of_type (v, G_VARIANT_TYPE_STRING));
g_hash_table_unref (found);
g_menu_remove_all (menu);
g_object_unref (menu);
g_object_unref (item);
}
static void
test_links (void)
{
GMenu *menu;
GMenuModel *m;
GMenuModel *x;
GMenuItem *item;
m = G_MENU_MODEL (g_menu_new ());
g_menu_append (G_MENU (m), "test", NULL);
menu = g_menu_new ();
item = g_menu_item_new ("test2", NULL);
g_menu_item_set_link (item, "submenu", m);
g_menu_prepend_item (menu, item);
g_object_unref (item);
item = g_menu_item_new ("test1", NULL);
g_menu_item_set_link (item, "section", m);
g_menu_insert_item (menu, 0, item);
g_object_unref (item);
item = g_menu_item_new ("test3", NULL);
g_menu_item_set_link (item, "wallet", m);
g_menu_insert_item (menu, 1000, item);
g_object_unref (item);
item = g_menu_item_new ("test4", NULL);
g_menu_item_set_link (item, "purse", m);
g_menu_item_set_link (item, "purse", NULL);
g_menu_append_item (menu, item);
g_object_unref (item);
g_assert_cmpint (g_menu_model_get_n_items (G_MENU_MODEL (menu)), ==, 4);
x = g_menu_model_get_item_link (G_MENU_MODEL (menu), 0, "section");
g_assert (x == m);
g_object_unref (x);
x = g_menu_model_get_item_link (G_MENU_MODEL (menu), 1, "submenu");
g_assert (x == m);
g_object_unref (x);
x = g_menu_model_get_item_link (G_MENU_MODEL (menu), 2, "wallet");
g_assert (x == m);
g_object_unref (x);
x = g_menu_model_get_item_link (G_MENU_MODEL (menu), 3, "purse");
g_assert (x == NULL);
g_object_unref (m);
g_object_unref (menu);
}
static void
test_mutable (void)
{
GMenu *menu;
menu = g_menu_new ();
g_menu_append (menu, "test", "test");
g_assert (g_menu_model_is_mutable (G_MENU_MODEL (menu)));
g_menu_freeze (menu);
g_assert (!g_menu_model_is_mutable (G_MENU_MODEL (menu)));
g_object_unref (menu);
}
static void
test_convenience (void)
{
GMenu *m1, *m2;
GMenu *sub;
GMenuItem *item;
m1 = g_menu_new ();
m2 = g_menu_new ();
sub = g_menu_new ();
g_menu_prepend (m1, "label1", "do::something");
g_menu_insert (m2, 0, "label1", "do::something");
g_menu_append (m1, "label2", "do::somethingelse");
g_menu_insert (m2, -1, "label2", "do::somethingelse");
g_menu_insert_section (m1, 10, "label3", G_MENU_MODEL (sub));
item = g_menu_item_new_section ("label3", G_MENU_MODEL (sub));
g_menu_insert_item (m2, 10, item);
g_object_unref (item);
g_menu_prepend_section (m1, "label4", G_MENU_MODEL (sub));
g_menu_insert_section (m2, 0, "label4", G_MENU_MODEL (sub));
g_menu_append_section (m1, "label5", G_MENU_MODEL (sub));
g_menu_insert_section (m2, -1, "label5", G_MENU_MODEL (sub));
g_menu_insert_submenu (m1, 5, "label6", G_MENU_MODEL (sub));
item = g_menu_item_new_submenu ("label6", G_MENU_MODEL (sub));
g_menu_insert_item (m2, 5, item);
g_object_unref (item);
g_menu_prepend_submenu (m1, "label7", G_MENU_MODEL (sub));
g_menu_insert_submenu (m2, 0, "label7", G_MENU_MODEL (sub));
g_menu_append_submenu (m1, "label8", G_MENU_MODEL (sub));
g_menu_insert_submenu (m2, -1, "label8", G_MENU_MODEL (sub));
assert_menus_equal (G_MENU_MODEL (m1), G_MENU_MODEL (m2));
g_object_unref (m1);
g_object_unref (m2);
g_object_unref (sub);
}
static void
test_menuitem (void)
{
GMenu *menu;
GMenu *submenu;
GMenuItem *item;
GIcon *icon;
gboolean b;
gchar *s;
menu = g_menu_new ();
submenu = g_menu_new ();
item = g_menu_item_new ("label", "action");
g_menu_item_set_attribute (item, "attribute", "b", TRUE);
g_menu_item_set_link (item, G_MENU_LINK_SUBMENU, G_MENU_MODEL (submenu));
g_menu_append_item (menu, item);
icon = g_themed_icon_new ("bla");
g_menu_item_set_icon (item, icon);
g_object_unref (icon);
g_assert (g_menu_item_get_attribute (item, "attribute", "b", &b));
g_assert (b);
g_menu_item_set_action_and_target (item, "action", "(bs)", TRUE, "string");
g_assert (g_menu_item_get_attribute (item, "target", "(bs)", &b, &s));
g_assert (b);
g_assert_cmpstr (s, ==, "string");
g_free (s);
g_object_unref (item);
item = g_menu_item_new_from_model (G_MENU_MODEL (menu), 0);
assert_menuitem_equal (item, G_MENU_MODEL (menu), 0);
g_object_unref (item);
g_object_unref (menu);
g_object_unref (submenu);
}
static GDBusInterfaceInfo *
org_gtk_Menus_get_interface (void)
{
static GDBusInterfaceInfo *interface_info;
if (interface_info == NULL)
{
GError *error = NULL;
GDBusNodeInfo *info;
info = g_dbus_node_info_new_for_xml ("<node>"
" <interface name='org.gtk.Menus'>"
" <method name='Start'>"
" <arg type='au' name='groups' direction='in'/>"
" <arg type='a(uuaa{sv})' name='content' direction='out'/>"
" </method>"
" <method name='End'>"
" <arg type='au' name='groups' direction='in'/>"
" </method>"
" <signal name='Changed'>"
" arg type='a(uuuuaa{sv})' name='changes'/>"
" </signal>"
" </interface>"
"</node>", &error);
if (info == NULL)
g_error ("%s\n", error->message);
interface_info = g_dbus_node_info_lookup_interface (info, "org.gtk.Menus");
g_assert (interface_info != NULL);
g_dbus_interface_info_ref (interface_info);
g_dbus_node_info_unref (info);
}
return interface_info;
}
static void
g_menu_exporter_method_call (GDBusConnection *connection,
const gchar *sender,
const gchar *object_path,
const gchar *interface_name,
const gchar *method_name,
GVariant *parameters,
GDBusMethodInvocation *invocation,
gpointer user_data)
{
const struct {
guint position;
guint removed;
} data[] = {
{ -2, 4 },
{ 0, 3 },
{ 4, 1 }
};
gsize i;
GError *error = NULL;
g_dbus_method_invocation_return_value (invocation, g_variant_new_parsed ("@(a(uuaa{sv})) ([(0, 0, [{ 'label': <'test'> }])],)"));
/* invalid signatures */
g_dbus_connection_emit_signal (connection, sender, "/", "org.gtk.Menus", "Changed",
g_variant_new_parsed ("([(1, 2, 3)],)"), &error);
g_assert_no_error (error);
/* add an item at an invalid position */
g_test_expect_message (G_LOG_DOMAIN, G_LOG_LEVEL_WARNING, "*invalid*");
g_dbus_connection_emit_signal (connection, sender, "/", "org.gtk.Menus", "Changed",
g_variant_new_parsed ("@(a(uuuuaa{sv})) ([(%u, %u, %u, %u, [{ 'label': <'test'> }])],)", 0, 0, 2, 0),
&error);
g_assert_no_error (error);
for (i = 0; i < G_N_ELEMENTS (data); i++)
{
GVariant *params;
g_test_expect_message (G_LOG_DOMAIN, G_LOG_LEVEL_WARNING, "*invalid*");
params = g_variant_new_parsed ("@(a(uuuuaa{sv})) ([(%u, %u, %u, %u, [])],)", 0, 0, data[i].position, data[i].removed);
g_dbus_connection_emit_signal (connection, sender, "/", "org.gtk.Menus", "Changed", params, &error);
g_assert_no_error (error);
}
}
static void
menu_changed (GMenuModel *menu,
gint position,
gint removed,
gint added,
gpointer user_data)
{
unsigned int *counter = user_data;
*counter += 1;
}
static void
test_input_validation (void)
{
const GDBusInterfaceVTable vtable = {
g_menu_exporter_method_call, NULL, NULL, { NULL, }
};
GError *error = NULL;
GDBusConnection *bus;
GDBusMenuModel *proxy;
guint id;
const gchar *bus_name;
GMainLoop *loop;
unsigned int n_signal_emissions = 0;
gulong signal_id;
g_test_bug ("https://gitlab.gnome.org/GNOME/glib/-/issues/861");
bus = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, &error);
g_assert_no_error (error);
id = g_dbus_connection_register_object (bus, "/", org_gtk_Menus_get_interface (),
&vtable, NULL, NULL, &error);
g_assert_no_error (error);
bus_name = g_dbus_connection_get_unique_name (bus);
proxy = g_dbus_menu_model_get (bus, bus_name, "/");
signal_id = g_signal_connect (proxy, "items-changed", G_CALLBACK (menu_changed), &n_signal_emissions);
/* get over laziness */
g_menu_model_get_n_items (G_MENU_MODEL (proxy));
loop = g_main_loop_new (NULL, FALSE);
g_timeout_add (100, stop_loop, loop);
g_main_loop_run (loop);
/* "items-changed" should only be emitted for the initial contents of
* the menu. Subsequent calls are all invalid.
*/
g_assert_cmpuint (n_signal_emissions, ==, 1);
g_test_assert_expected_messages ();
g_main_loop_unref (loop);
g_dbus_connection_unregister_object (bus, id);
g_signal_handler_disconnect (proxy, signal_id);
g_object_unref (proxy);
g_object_unref (bus);
}
/* Epilogue {{{1 */
int
main (int argc, char **argv)
{
gboolean ret;
g_test_init (&argc, &argv, NULL);
session_bus_up ();
g_test_add_func ("/gmenu/equality", test_equality);
g_test_add_func ("/gmenu/random", test_random);
g_test_add_func ("/gmenu/dbus/roundtrip", test_dbus_roundtrip);
g_test_add_func ("/gmenu/dbus/subscriptions", test_dbus_subscriptions);
g_test_add_func ("/gmenu/dbus/threaded", test_dbus_threaded);
g_test_add_func ("/gmenu/dbus/peer/roundtrip", test_dbus_peer_roundtrip);
g_test_add_func ("/gmenu/dbus/peer/subscriptions", test_dbus_peer_subscriptions);
g_test_add_func ("/gmenu/dbus/export/error-handling", test_dbus_export_error_handling);
g_test_add_func ("/gmenu/attributes", test_attributes);
g_test_add_func ("/gmenu/attributes/iterate", test_attribute_iter);
g_test_add_func ("/gmenu/links", test_links);
g_test_add_func ("/gmenu/mutable", test_mutable);
g_test_add_func ("/gmenu/convenience", test_convenience);
g_test_add_func ("/gmenu/menuitem", test_menuitem);
g_test_add_func ("/gmenu/input-validation", test_input_validation);
ret = g_test_run ();
session_bus_down ();
return ret;
}
/* vim:set foldmethod=marker: */