From cba851fc272e1f5ea1c319a9574e891d866ba76a Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Mon, 16 Mar 2020 12:13:35 +0000 Subject: [PATCH 1/2] app-filter: Add serialize and deserialize methods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add methods to serialise and deserialise the app filter, and use them to replace the code in `MctManager` which was previously doing this. This exposes the variant format for the app filter in the API (although the format is described as ‘opaque’) so that user code can log it or store it separately. Signed-off-by: Philip Withnall --- libmalcontent/app-filter.c | 170 +++++++++++++++++++++++- libmalcontent/app-filter.h | 5 + libmalcontent/manager.c | 219 +++++-------------------------- libmalcontent/tests/app-filter.c | 81 ++++++++++++ 4 files changed, 287 insertions(+), 188 deletions(-) diff --git a/libmalcontent/app-filter.c b/libmalcontent/app-filter.c index 3599424..a9d9683 100644 --- a/libmalcontent/app-filter.c +++ b/libmalcontent/app-filter.c @@ -96,7 +96,7 @@ mct_app_filter_unref (MctAppFilter *filter) * * Get the user ID of the user this #MctAppFilter is for. * - * Returns: user ID of the relevant user + * Returns: user ID of the relevant user, or `(uid_t) -1` if unknown * Since: 0.2.0 */ uid_t @@ -533,6 +533,174 @@ mct_app_filter_is_system_installation_allowed (MctAppFilter *filter) return filter->allow_system_installation; } +/** + * _mct_app_filter_build_app_filter_variant: + * @filter: an #MctAppFilter + * + * Build a #GVariant which contains the app filter from @filter, in the format + * used for storing it in AccountsService. + * + * Returns: (transfer floating): a new, floating #GVariant containing the app + * filter + */ +static GVariant * +_mct_app_filter_build_app_filter_variant (MctAppFilter *filter) +{ + g_auto(GVariantBuilder) builder = G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE ("(bas)")); + + g_return_val_if_fail (filter != NULL, NULL); + g_return_val_if_fail (filter->ref_count >= 1, NULL); + + g_variant_builder_add (&builder, "b", + (filter->app_list_type == MCT_APP_FILTER_LIST_WHITELIST)); + g_variant_builder_open (&builder, G_VARIANT_TYPE ("as")); + + for (gsize i = 0; filter->app_list[i] != NULL; i++) + g_variant_builder_add (&builder, "s", filter->app_list[i]); + + g_variant_builder_close (&builder); + + return g_variant_builder_end (&builder); +} + +/** + * mct_app_filter_serialize: + * @filter: an #MctAppFilter + * + * Build a #GVariant which contains the app filter from @filter, in an opaque + * variant format. This format may change in future, but + * mct_app_filter_deserialize() is guaranteed to always be able to load any + * variant produced by the current or any previous version of + * mct_app_filter_serialize(). + * + * Returns: (transfer floating): a new, floating #GVariant containing the app + * filter + * Since: 0.7.0 + */ +GVariant * +mct_app_filter_serialize (MctAppFilter *filter) +{ + g_auto(GVariantBuilder) builder = G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE ("a{sv}")); + + g_return_val_if_fail (filter != NULL, NULL); + g_return_val_if_fail (filter->ref_count >= 1, NULL); + + /* The serialisation format is exactly the + * `com.endlessm.ParentalControls.AppFilter` D-Bus interface. */ + g_variant_builder_add (&builder, "{sv}", "AppFilter", + _mct_app_filter_build_app_filter_variant (filter)); + g_variant_builder_add (&builder, "{sv}", "OarsFilter", + g_variant_new ("(s@a{ss})", "oars-1.1", + filter->oars_ratings)); + g_variant_builder_add (&builder, "{sv}", "AllowUserInstallation", + g_variant_new_boolean (filter->allow_user_installation)); + g_variant_builder_add (&builder, "{sv}", "AllowSystemInstallation", + g_variant_new_boolean (filter->allow_system_installation)); + + return g_variant_builder_end (&builder); +} + +/** + * mct_app_filter_deserialize: + * @variant: a serialized app filter variant + * @user_id: the ID of the user the app filter relates to + * @error: return location for a #GError, or %NULL + * + * Deserialize an app filter previously serialized with + * mct_app_filter_serialize(). This function guarantees to be able to + * deserialize any serialized form from this version or older versions of + * libmalcontent. + * + * If deserialization fails, %MCT_MANAGER_ERROR_INVALID_DATA will be returned. + * + * Returns: (transfer full): deserialized app filter + * Since: 0.7.0 + */ +MctAppFilter * +mct_app_filter_deserialize (GVariant *variant, + uid_t user_id, + GError **error) +{ + gboolean is_whitelist; + g_auto(GStrv) app_list = NULL; + const gchar *content_rating_kind; + g_autoptr(GVariant) oars_variant = NULL; + gboolean allow_user_installation; + gboolean allow_system_installation; + g_autoptr(MctAppFilter) app_filter = NULL; + + g_return_val_if_fail (variant != NULL, NULL); + g_return_val_if_fail (error == NULL || *error == NULL, NULL); + + /* Check the overall type. */ + if (!g_variant_is_of_type (variant, G_VARIANT_TYPE ("a{sv}"))) + { + g_set_error (error, MCT_MANAGER_ERROR, + MCT_MANAGER_ERROR_INVALID_DATA, + _("App filter for user %u was in an unrecognized format"), + (guint) user_id); + return NULL; + } + + /* Extract the properties we care about. The default values here should be + * kept in sync with those in the `com.endlessm.ParentalControls.AppFilter` + * D-Bus interface. */ + if (!g_variant_lookup (variant, "AppFilter", "(b^as)", + &is_whitelist, &app_list)) + { + /* Default value. */ + is_whitelist = FALSE; + app_list = g_new0 (gchar *, 1); + } + + if (!g_variant_lookup (variant, "OarsFilter", "(&s@a{ss})", + &content_rating_kind, &oars_variant)) + { + /* Default value. */ + content_rating_kind = "oars-1.1"; + oars_variant = g_variant_new ("a{ss}", NULL); + } + + /* Check that the OARS filter is in a format we support. Currently, that’s + * only oars-1.0 and oars-1.1. */ + if (!g_str_equal (content_rating_kind, "oars-1.0") && + !g_str_equal (content_rating_kind, "oars-1.1")) + { + g_set_error (error, MCT_MANAGER_ERROR, + MCT_MANAGER_ERROR_INVALID_DATA, + _("OARS filter for user %u has an unrecognized kind ‘%s’"), + (guint) user_id, content_rating_kind); + return NULL; + } + + if (!g_variant_lookup (variant, "AllowUserInstallation", "b", + &allow_user_installation)) + { + /* Default value. */ + allow_user_installation = TRUE; + } + + if (!g_variant_lookup (variant, "AllowSystemInstallation", "b", + &allow_system_installation)) + { + /* Default value. */ + allow_system_installation = FALSE; + } + + /* Success. Create an #MctAppFilter object to contain the results. */ + app_filter = g_new0 (MctAppFilter, 1); + app_filter->ref_count = 1; + app_filter->user_id = user_id; + app_filter->app_list = g_steal_pointer (&app_list); + app_filter->app_list_type = + is_whitelist ? MCT_APP_FILTER_LIST_WHITELIST : MCT_APP_FILTER_LIST_BLACKLIST; + app_filter->oars_ratings = g_steal_pointer (&oars_variant); + app_filter->allow_user_installation = allow_user_installation; + app_filter->allow_system_installation = allow_system_installation; + + return g_steal_pointer (&app_filter); +} + /* * Actual implementation of #MctAppFilterBuilder. * diff --git a/libmalcontent/app-filter.h b/libmalcontent/app-filter.h index 1860fe0..b5d8fb9 100644 --- a/libmalcontent/app-filter.h +++ b/libmalcontent/app-filter.h @@ -96,6 +96,11 @@ MctAppFilterOarsValue mct_app_filter_get_oars_value (MctAppFilter *filter, gboolean mct_app_filter_is_user_installation_allowed (MctAppFilter *filter); gboolean mct_app_filter_is_system_installation_allowed (MctAppFilter *filter); +GVariant *mct_app_filter_serialize (MctAppFilter *filter); +MctAppFilter *mct_app_filter_deserialize (GVariant *variant, + uid_t user_id, + GError **error); + /** * MctAppFilterBuilder: * diff --git a/libmalcontent/manager.c b/libmalcontent/manager.c index eef42fb..3bed6a3 100644 --- a/libmalcontent/manager.c +++ b/libmalcontent/manager.c @@ -257,37 +257,6 @@ _mct_manager_user_changed_cb (GDBusConnection *connection, g_signal_emit_by_name (manager, "app-filter-changed", uid); } -/** - * _mct_app_filter_build_app_filter_variant: - * @filter: an #MctAppFilter - * - * Build a #GVariant which contains the app filter from @filter, in the format - * used for storing it in AccountsService. - * - * Returns: (transfer floating): a new, floating #GVariant containing the app - * filter - * Since: 0.2.0 - */ -static GVariant * -_mct_app_filter_build_app_filter_variant (MctAppFilter *filter) -{ - g_auto(GVariantBuilder) builder = G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE ("(bas)")); - - g_return_val_if_fail (filter != NULL, NULL); - g_return_val_if_fail (filter->ref_count >= 1, NULL); - - g_variant_builder_add (&builder, "b", - (filter->app_list_type == MCT_APP_FILTER_LIST_WHITELIST)); - g_variant_builder_open (&builder, G_VARIANT_TYPE ("as")); - - for (gsize i = 0; filter->app_list[i] != NULL; i++) - g_variant_builder_add (&builder, "s", filter->app_list[i]); - - g_variant_builder_close (&builder); - - return g_variant_builder_end (&builder); -} - /* Check if @error is a D-Bus remote error matching @expected_error_name. */ static gboolean bus_remote_error_matches (const GError *error, @@ -386,13 +355,6 @@ mct_manager_get_app_filter (MctManager *self, g_autoptr(GVariant) result_variant = NULL; g_autoptr(GVariant) properties = NULL; g_autoptr(GError) local_error = NULL; - g_autoptr(MctAppFilter) app_filter = NULL; - gboolean is_whitelist; - g_auto(GStrv) app_list = NULL; - const gchar *content_rating_kind; - g_autoptr(GVariant) oars_variant = NULL; - gboolean allow_user_installation; - gboolean allow_system_installation; g_return_val_if_fail (MCT_IS_MANAGER (self), NULL); g_return_val_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable), NULL); @@ -443,8 +405,7 @@ mct_manager_get_app_filter (MctManager *self, /* Extract the properties we care about. They may be silently omitted from the * results if we don’t have permission to access them. */ properties = g_variant_get_child_value (result_variant, 0); - if (!g_variant_lookup (properties, "AppFilter", "(b^as)", - &is_whitelist, &app_list)) + if (!g_variant_lookup (properties, "AppFilter", "(b^as)", NULL, NULL)) { g_set_error (error, MCT_MANAGER_ERROR, MCT_MANAGER_ERROR_PERMISSION_DENIED, @@ -453,52 +414,7 @@ mct_manager_get_app_filter (MctManager *self, return NULL; } - if (!g_variant_lookup (properties, "OarsFilter", "(&s@a{ss})", - &content_rating_kind, &oars_variant)) - { - /* Default value. */ - content_rating_kind = "oars-1.1"; - oars_variant = g_variant_new ("a{ss}", NULL); - } - - /* Check that the OARS filter is in a format we support. Currently, that’s - * only oars-1.0 and oars-1.1. */ - if (!g_str_equal (content_rating_kind, "oars-1.0") && - !g_str_equal (content_rating_kind, "oars-1.1")) - { - g_set_error (error, MCT_MANAGER_ERROR, - MCT_MANAGER_ERROR_INVALID_DATA, - _("OARS filter for user %u has an unrecognized kind ‘%s’"), - (guint) user_id, content_rating_kind); - return NULL; - } - - if (!g_variant_lookup (properties, "AllowUserInstallation", "b", - &allow_user_installation)) - { - /* Default value. */ - allow_user_installation = TRUE; - } - - if (!g_variant_lookup (properties, "AllowSystemInstallation", "b", - &allow_system_installation)) - { - /* Default value. */ - allow_system_installation = FALSE; - } - - /* Success. Create an #MctAppFilter object to contain the results. */ - app_filter = g_new0 (MctAppFilter, 1); - app_filter->ref_count = 1; - app_filter->user_id = user_id; - app_filter->app_list = g_steal_pointer (&app_list); - app_filter->app_list_type = - is_whitelist ? MCT_APP_FILTER_LIST_WHITELIST : MCT_APP_FILTER_LIST_BLACKLIST; - app_filter->oars_ratings = g_steal_pointer (&oars_variant); - app_filter->allow_user_installation = allow_user_installation; - app_filter->allow_system_installation = allow_system_installation; - - return g_steal_pointer (&app_filter); + return mct_app_filter_deserialize (properties, user_id, error); } static void get_app_filter_thread_cb (GTask *task, @@ -632,14 +548,10 @@ mct_manager_set_app_filter (MctManager *self, GError **error) { g_autofree gchar *object_path = NULL; - g_autoptr(GVariant) app_filter_variant = NULL; - g_autoptr(GVariant) oars_filter_variant = NULL; - g_autoptr(GVariant) allow_user_installation_variant = NULL; - g_autoptr(GVariant) allow_system_installation_variant = NULL; - g_autoptr(GVariant) app_filter_result_variant = NULL; - g_autoptr(GVariant) oars_filter_result_variant = NULL; - g_autoptr(GVariant) allow_user_installation_result_variant = NULL; - g_autoptr(GVariant) allow_system_installation_result_variant = NULL; + g_autoptr(GVariant) properties_variant = NULL; + g_autoptr(GVariant) properties_value = NULL; + const gchar *properties_key = NULL; + GVariantIter iter; g_autoptr(GError) local_error = NULL; g_return_val_if_fail (MCT_IS_MANAGER (self), FALSE); @@ -654,102 +566,35 @@ mct_manager_set_app_filter (MctManager *self, if (object_path == NULL) return FALSE; - app_filter_variant = _mct_app_filter_build_app_filter_variant (app_filter); - oars_filter_variant = g_variant_new ("(s@a{ss})", "oars-1.1", - app_filter->oars_ratings); - allow_user_installation_variant = g_variant_new_boolean (app_filter->allow_user_installation); - allow_system_installation_variant = g_variant_new_boolean (app_filter->allow_system_installation); + properties_variant = mct_app_filter_serialize (app_filter); - app_filter_result_variant = - g_dbus_connection_call_sync (self->connection, - "org.freedesktop.Accounts", - object_path, - "org.freedesktop.DBus.Properties", - "Set", - g_variant_new ("(ssv)", - "com.endlessm.ParentalControls.AppFilter", - "AppFilter", - g_steal_pointer (&app_filter_variant)), - G_VARIANT_TYPE ("()"), - (flags & MCT_MANAGER_SET_VALUE_FLAGS_INTERACTIVE) - ? G_DBUS_CALL_FLAGS_ALLOW_INTERACTIVE_AUTHORIZATION - : G_DBUS_CALL_FLAGS_NONE, - -1, /* timeout, ms */ - cancellable, - &local_error); - if (local_error != NULL) + g_variant_iter_init (&iter, properties_variant); + while (g_variant_iter_loop (&iter, "{&sv}", &properties_key, &properties_value)) { - g_propagate_error (error, bus_error_to_manager_error (local_error, user_id)); - return FALSE; - } + g_autoptr(GVariant) result_variant = NULL; - oars_filter_result_variant = - g_dbus_connection_call_sync (self->connection, - "org.freedesktop.Accounts", - object_path, - "org.freedesktop.DBus.Properties", - "Set", - g_variant_new ("(ssv)", - "com.endlessm.ParentalControls.AppFilter", - "OarsFilter", - g_steal_pointer (&oars_filter_variant)), - G_VARIANT_TYPE ("()"), - (flags & MCT_MANAGER_SET_VALUE_FLAGS_INTERACTIVE) - ? G_DBUS_CALL_FLAGS_ALLOW_INTERACTIVE_AUTHORIZATION - : G_DBUS_CALL_FLAGS_NONE, - -1, /* timeout, ms */ - cancellable, - &local_error); - if (local_error != NULL) - { - g_propagate_error (error, bus_error_to_manager_error (local_error, user_id)); - return FALSE; - } - - allow_user_installation_result_variant = - g_dbus_connection_call_sync (self->connection, - "org.freedesktop.Accounts", - object_path, - "org.freedesktop.DBus.Properties", - "Set", - g_variant_new ("(ssv)", - "com.endlessm.ParentalControls.AppFilter", - "AllowUserInstallation", - g_steal_pointer (&allow_user_installation_variant)), - G_VARIANT_TYPE ("()"), - (flags & MCT_MANAGER_SET_VALUE_FLAGS_INTERACTIVE) - ? G_DBUS_CALL_FLAGS_ALLOW_INTERACTIVE_AUTHORIZATION - : G_DBUS_CALL_FLAGS_NONE, - -1, /* timeout, ms */ - cancellable, - &local_error); - if (local_error != NULL) - { - g_propagate_error (error, bus_error_to_manager_error (local_error, user_id)); - return FALSE; - } - - allow_system_installation_result_variant = - g_dbus_connection_call_sync (self->connection, - "org.freedesktop.Accounts", - object_path, - "org.freedesktop.DBus.Properties", - "Set", - g_variant_new ("(ssv)", - "com.endlessm.ParentalControls.AppFilter", - "AllowSystemInstallation", - g_steal_pointer (&allow_system_installation_variant)), - G_VARIANT_TYPE ("()"), - (flags & MCT_MANAGER_SET_VALUE_FLAGS_INTERACTIVE) - ? G_DBUS_CALL_FLAGS_ALLOW_INTERACTIVE_AUTHORIZATION - : G_DBUS_CALL_FLAGS_NONE, - -1, /* timeout, ms */ - cancellable, - &local_error); - if (local_error != NULL) - { - g_propagate_error (error, bus_error_to_manager_error (local_error, user_id)); - return FALSE; + result_variant = + g_dbus_connection_call_sync (self->connection, + "org.freedesktop.Accounts", + object_path, + "org.freedesktop.DBus.Properties", + "Set", + g_variant_new ("(ssv)", + "com.endlessm.ParentalControls.AppFilter", + properties_key, + properties_value), + G_VARIANT_TYPE ("()"), + (flags & MCT_MANAGER_SET_VALUE_FLAGS_INTERACTIVE) + ? G_DBUS_CALL_FLAGS_ALLOW_INTERACTIVE_AUTHORIZATION + : G_DBUS_CALL_FLAGS_NONE, + -1, /* timeout, ms */ + cancellable, + &local_error); + if (local_error != NULL) + { + g_propagate_error (error, bus_error_to_manager_error (local_error, user_id)); + return FALSE; + } } return TRUE; diff --git a/libmalcontent/tests/app-filter.c b/libmalcontent/tests/app-filter.c index 7ce4abc..f5f9eca 100644 --- a/libmalcontent/tests/app-filter.c +++ b/libmalcontent/tests/app-filter.c @@ -85,6 +85,83 @@ test_app_filter_refs (void) /* Final ref is dropped by g_autoptr(). */ } +/* Basic test of mct_app_filter_serialize() on an app filter. */ +static void +test_app_filter_serialize (void) +{ + g_auto(MctAppFilterBuilder) builder = MCT_APP_FILTER_BUILDER_INIT (); + g_autoptr(MctAppFilter) filter = NULL; + g_autoptr(GVariant) serialized = NULL; + + /* Use an empty #MctAppFilter. */ + filter = mct_app_filter_builder_end (&builder); + + /* We can’t assert anything about the serialisation format, since it’s opaque. */ + serialized = mct_app_filter_serialize (filter); + g_assert_nonnull (serialized); +} + +/* Basic test of mct_app_filter_deserialize() on various current and historic + * serialised app filter variants. */ +static void +test_app_filter_deserialize (void) +{ + /* These are all opaque. Older versions should be kept around to test + * backwards compatibility. */ + const gchar *valid_app_filters[] = + { + "@a{sv} {}", + "{ 'AppFilter': <(true, @as [])> }", + "{ 'OarsFilter': <('oars-1.1', { 'violence-cartoon': 'mild' })> }", + "{ 'AllowUserInstallation': }", + "{ 'AllowSystemInstallation': }", + }; + + for (gsize i = 0; i < G_N_ELEMENTS (valid_app_filters); i++) + { + g_autoptr(GVariant) serialized = NULL; + g_autoptr(MctAppFilter) filter = NULL; + g_autoptr(GError) local_error = NULL; + + g_test_message ("%" G_GSIZE_FORMAT ": %s", i, valid_app_filters[i]); + + serialized = g_variant_parse (NULL, valid_app_filters[i], NULL, NULL, NULL); + g_assert (serialized != NULL); + + filter = mct_app_filter_deserialize (serialized, 1, &local_error); + g_assert_no_error (local_error); + g_assert_nonnull (filter); + } +} + +/* Test of mct_app_filter_deserialize() on various invalid variants. */ +static void +test_app_filter_deserialize_invalid (void) +{ + const gchar *invalid_app_filters[] = + { + "false", + "()", + "{ 'OarsFilter': <('invalid', { 'violence-cartoon': 'mild' })> }", + }; + + for (gsize i = 0; i < G_N_ELEMENTS (invalid_app_filters); i++) + { + g_autoptr(GVariant) serialized = NULL; + g_autoptr(MctAppFilter) filter = NULL; + g_autoptr(GError) local_error = NULL; + + g_test_message ("%" G_GSIZE_FORMAT ": %s", i, invalid_app_filters[i]); + + serialized = g_variant_parse (NULL, invalid_app_filters[i], NULL, NULL, NULL); + g_assert (serialized != NULL); + + filter = mct_app_filter_deserialize (serialized, 1, &local_error); + g_assert_error (local_error, MCT_MANAGER_ERROR, MCT_MANAGER_ERROR_INVALID_DATA); + g_assert_null (filter); + } +} + /* Fixture for tests which use an #MctAppFilterBuilder. The builder can either * be heap- or stack-allocated. @builder will always be a valid pointer to it. */ @@ -1381,6 +1458,10 @@ main (int argc, g_test_add_func ("/app-filter/types", test_app_filter_types); g_test_add_func ("/app-filter/refs", test_app_filter_refs); + g_test_add_func ("/app-filter/serialize", test_app_filter_serialize); + g_test_add_func ("/app-filter/deserialize", test_app_filter_deserialize); + g_test_add_func ("/app-filter/deserialize/invalid", test_app_filter_deserialize_invalid); + g_test_add ("/app-filter/builder/stack/non-empty", BuilderFixture, NULL, builder_set_up_stack, test_app_filter_builder_non_empty, builder_tear_down_stack); From 6ba767029f1d45b9be11bc1c59b9ed1f574fd26e Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Mon, 16 Mar 2020 12:15:18 +0000 Subject: [PATCH 2/2] session-limits: Add serialize and deserialize methods See the previous commit; this is the same, but for session limits. Signed-off-by: Philip Withnall --- libmalcontent/manager.c | 94 +++++------------ libmalcontent/session-limits.c | 146 ++++++++++++++++++++++++++- libmalcontent/session-limits.h | 5 + libmalcontent/tests/session-limits.c | 82 +++++++++++++++ 4 files changed, 255 insertions(+), 72 deletions(-) diff --git a/libmalcontent/manager.c b/libmalcontent/manager.c index 3bed6a3..62c340e 100644 --- a/libmalcontent/manager.c +++ b/libmalcontent/manager.c @@ -738,9 +738,6 @@ mct_manager_get_session_limits (MctManager *self, g_autoptr(GVariant) result_variant = NULL; g_autoptr(GVariant) properties = NULL; g_autoptr(GError) local_error = NULL; - g_autoptr(MctSessionLimits) session_limits = NULL; - guint32 limit_type; - guint32 daily_start_time, daily_end_time; g_return_val_if_fail (MCT_IS_MANAGER (self), NULL); g_return_val_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable), NULL); @@ -791,8 +788,7 @@ mct_manager_get_session_limits (MctManager *self, /* Extract the properties we care about. They may be silently omitted from the * results if we don’t have permission to access them. */ properties = g_variant_get_child_value (result_variant, 0); - if (!g_variant_lookup (properties, "LimitType", "u", - &limit_type)) + if (!g_variant_lookup (properties, "LimitType", "u", NULL)) { g_set_error (error, MCT_MANAGER_ERROR, MCT_MANAGER_ERROR_PERMISSION_DENIED, @@ -801,45 +797,7 @@ mct_manager_get_session_limits (MctManager *self, return NULL; } - /* Check that the limit type is something we support. */ - G_STATIC_ASSERT (sizeof (limit_type) >= sizeof (MctSessionLimitsType)); - - if ((guint) limit_type > MCT_SESSION_LIMITS_TYPE_DAILY_SCHEDULE) - { - g_set_error (error, MCT_MANAGER_ERROR, - MCT_MANAGER_ERROR_INVALID_DATA, - _("Session limit for user %u has an unrecognized type ‘%u’"), - (guint) user_id, limit_type); - return NULL; - } - - if (!g_variant_lookup (properties, "DailySchedule", "(uu)", - &daily_start_time, &daily_end_time)) - { - /* Default value. */ - daily_start_time = 0; - daily_end_time = 24 * 60 * 60; - } - - if (daily_start_time >= daily_end_time || - daily_end_time > 24 * 60 * 60) - { - g_set_error (error, MCT_MANAGER_ERROR, - MCT_MANAGER_ERROR_INVALID_DATA, - _("Session limit for user %u has invalid daily schedule %u–%u"), - (guint) user_id, daily_start_time, daily_end_time); - return NULL; - } - - /* Success. Create an #MctSessionLimits object to contain the results. */ - session_limits = g_new0 (MctSessionLimits, 1); - session_limits->ref_count = 1; - session_limits->user_id = user_id; - session_limits->limit_type = limit_type; - session_limits->daily_start_time = daily_start_time; - session_limits->daily_end_time = daily_end_time; - - return g_steal_pointer (&session_limits); + return mct_session_limits_deserialize (properties, user_id, error); } static void get_session_limits_thread_cb (GTask *task, @@ -973,11 +931,12 @@ mct_manager_set_session_limits (MctManager *self, GError **error) { g_autofree gchar *object_path = NULL; - g_autoptr(GVariant) limit_variant = NULL; - const gchar *limit_property_name = NULL; g_autoptr(GVariant) limit_type_variant = NULL; - g_autoptr(GVariant) limit_result_variant = NULL; g_autoptr(GVariant) limit_type_result_variant = NULL; + g_autoptr(GVariant) properties_variant = NULL; + g_autoptr(GVariant) properties_value = NULL; + const gchar *properties_key = NULL; + GVariantIter iter; g_autoptr(GError) local_error = NULL; g_return_val_if_fail (MCT_IS_MANAGER (self), FALSE); @@ -992,29 +951,22 @@ mct_manager_set_session_limits (MctManager *self, if (object_path == NULL) return FALSE; - switch (session_limits->limit_type) - { - case MCT_SESSION_LIMITS_TYPE_DAILY_SCHEDULE: - limit_variant = g_variant_new ("(uu)", - session_limits->daily_start_time, - session_limits->daily_end_time); - limit_property_name = "DailySchedule"; - break; - case MCT_SESSION_LIMITS_TYPE_NONE: - limit_variant = NULL; - limit_property_name = NULL; - break; - default: - g_assert_not_reached (); - } + properties_variant = mct_session_limits_serialize (session_limits); - limit_type_variant = g_variant_new_uint32 (session_limits->limit_type); - - if (limit_property_name != NULL) + g_variant_iter_init (&iter, properties_variant); + while (g_variant_iter_loop (&iter, "{&sv}", &properties_key, &properties_value)) { - /* Change the details of the new limit first, so that all the properties are - * correct by the time the limit type is changed over. */ - limit_result_variant = + g_autoptr(GVariant) result_variant = NULL; + + /* Change the limit type last, so all the details of the new limit are + * correct by the time it’s changed over. */ + if (g_str_equal (properties_key, "LimitType")) + { + limit_type_variant = g_steal_pointer (&properties_value); + continue; + } + + result_variant = g_dbus_connection_call_sync (self->connection, "org.freedesktop.Accounts", object_path, @@ -1022,8 +974,8 @@ mct_manager_set_session_limits (MctManager *self, "Set", g_variant_new ("(ssv)", "com.endlessm.ParentalControls.SessionLimits", - limit_property_name, - g_steal_pointer (&limit_variant)), + properties_key, + properties_value), G_VARIANT_TYPE ("()"), (flags & MCT_MANAGER_SET_VALUE_FLAGS_INTERACTIVE) ? G_DBUS_CALL_FLAGS_ALLOW_INTERACTIVE_AUTHORIZATION @@ -1047,7 +999,7 @@ mct_manager_set_session_limits (MctManager *self, g_variant_new ("(ssv)", "com.endlessm.ParentalControls.SessionLimits", "LimitType", - g_steal_pointer (&limit_type_variant)), + limit_type_variant), G_VARIANT_TYPE ("()"), (flags & MCT_MANAGER_SET_VALUE_FLAGS_INTERACTIVE) ? G_DBUS_CALL_FLAGS_ALLOW_INTERACTIVE_AUTHORIZATION diff --git a/libmalcontent/session-limits.c b/libmalcontent/session-limits.c index be4373f..dbe07ce 100644 --- a/libmalcontent/session-limits.c +++ b/libmalcontent/session-limits.c @@ -26,6 +26,7 @@ #include #include #include +#include #include #include "libmalcontent/session-limits-private.h" @@ -85,7 +86,7 @@ mct_session_limits_unref (MctSessionLimits *limits) * * Get the user ID of the user this #MctSessionLimits is for. * - * Returns: user ID of the relevant user + * Returns: user ID of the relevant user, or `(uid_t) -1` if unknown * Since: 0.5.0 */ uid_t @@ -190,6 +191,149 @@ out: return user_allowed_now; } +/** + * mct_session_limits_serialize: + * @limits: an #MctSessionLimits + * + * Build a #GVariant which contains the session limits from @limits, in an + * opaque variant format. This format may change in future, but + * mct_session_limits_deserialize() is guaranteed to always be able to load any + * variant produced by the current or any previous version of + * mct_session_limits_serialize(). + * + * Returns: (transfer floating): a new, floating #GVariant containing the + * session limits + * Since: 0.7.0 + */ +GVariant * +mct_session_limits_serialize (MctSessionLimits *limits) +{ + g_auto(GVariantBuilder) builder = G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE ("a{sv}")); + g_autoptr(GVariant) limit_variant = NULL; + const gchar *limit_property_name; + + g_return_val_if_fail (limits != NULL, NULL); + g_return_val_if_fail (limits->ref_count >= 1, NULL); + + /* The serialisation format is exactly the + * `com.endlessm.ParentalControls.SessionLimits` D-Bus interface. */ + switch (limits->limit_type) + { + case MCT_SESSION_LIMITS_TYPE_DAILY_SCHEDULE: + limit_variant = g_variant_new ("(uu)", + limits->daily_start_time, + limits->daily_end_time); + limit_property_name = "DailySchedule"; + break; + case MCT_SESSION_LIMITS_TYPE_NONE: + limit_variant = NULL; + limit_property_name = NULL; + break; + default: + g_assert_not_reached (); + } + + if (limit_property_name != NULL) + { + g_variant_builder_add (&builder, "{sv}", limit_property_name, + g_steal_pointer (&limit_variant)); + } + + g_variant_builder_add (&builder, "{sv}", "LimitType", + g_variant_new_uint32 (limits->limit_type)); + + return g_variant_builder_end (&builder); +} + +/** + * mct_session_limits_deserialize: + * @variant: a serialized session limits variant + * @user_id: the ID of the user the session limits relate to + * @error: return location for a #GError, or %NULL + * + * Deserialize a set of session limits previously serialized with + * mct_session_limits_serialize(). This function guarantees to be able to + * deserialize any serialized form from this version or older versions of + * libmalcontent. + * + * If deserialization fails, %MCT_MANAGER_ERROR_INVALID_DATA will be returned. + * + * Returns: (transfer full): deserialized session limits + * Since: 0.7.0 + */ +MctSessionLimits * +mct_session_limits_deserialize (GVariant *variant, + uid_t user_id, + GError **error) +{ + g_autoptr(MctSessionLimits) session_limits = NULL; + guint32 limit_type; + guint32 daily_start_time, daily_end_time; + + g_return_val_if_fail (variant != NULL, NULL); + g_return_val_if_fail (error == NULL || *error == NULL, NULL); + + /* Check the overall type. */ + if (!g_variant_is_of_type (variant, G_VARIANT_TYPE ("a{sv}"))) + { + g_set_error (error, MCT_MANAGER_ERROR, + MCT_MANAGER_ERROR_INVALID_DATA, + _("Session limit for user %u was in an unrecognized format"), + (guint) user_id); + return NULL; + } + + /* Extract the properties we care about. The default values here should be + * kept in sync with those in the `com.endlessm.ParentalControls.SessionLimits` + * D-Bus interface. */ + if (!g_variant_lookup (variant, "LimitType", "u", + &limit_type)) + { + /* Default value. */ + limit_type = MCT_SESSION_LIMITS_TYPE_NONE; + } + + /* Check that the limit type is something we support. */ + G_STATIC_ASSERT (sizeof (limit_type) >= sizeof (MctSessionLimitsType)); + + if ((guint) limit_type > MCT_SESSION_LIMITS_TYPE_DAILY_SCHEDULE) + { + g_set_error (error, MCT_MANAGER_ERROR, + MCT_MANAGER_ERROR_INVALID_DATA, + _("Session limit for user %u has an unrecognized type ‘%u’"), + (guint) user_id, limit_type); + return NULL; + } + + if (!g_variant_lookup (variant, "DailySchedule", "(uu)", + &daily_start_time, &daily_end_time)) + { + /* Default value. */ + daily_start_time = 0; + daily_end_time = 24 * 60 * 60; + } + + if (daily_start_time >= daily_end_time || + daily_end_time > 24 * 60 * 60) + { + g_set_error (error, MCT_MANAGER_ERROR, + MCT_MANAGER_ERROR_INVALID_DATA, + _("Session limit for user %u has invalid daily schedule %u–%u"), + (guint) user_id, daily_start_time, daily_end_time); + return NULL; + } + + /* Success. Create an #MctSessionLimits object to contain the results. */ + session_limits = g_new0 (MctSessionLimits, 1); + session_limits->ref_count = 1; + session_limits->user_id = user_id; + session_limits->limit_type = limit_type; + session_limits->daily_start_time = daily_start_time; + session_limits->daily_end_time = daily_end_time; + + return g_steal_pointer (&session_limits); +} + /* * Actual implementation of #MctSessionLimitsBuilder. * diff --git a/libmalcontent/session-limits.h b/libmalcontent/session-limits.h index d8fb5f1..dd5e9e9 100644 --- a/libmalcontent/session-limits.h +++ b/libmalcontent/session-limits.h @@ -57,6 +57,11 @@ gboolean mct_session_limits_check_time_remaining (MctSessionLimits *limits, guint64 *time_remaining_secs_out, gboolean *time_limit_enabled_out); +GVariant *mct_session_limits_serialize (MctSessionLimits *limits); +MctSessionLimits *mct_session_limits_deserialize (GVariant *variant, + uid_t user_id, + GError **error); + /** * MctSessionLimitsBuilder: * diff --git a/libmalcontent/tests/session-limits.c b/libmalcontent/tests/session-limits.c index 2559f3a..2a3e8bf 100644 --- a/libmalcontent/tests/session-limits.c +++ b/libmalcontent/tests/session-limits.c @@ -92,6 +92,84 @@ test_session_limits_check_time_remaining_invalid_time (void) g_assert_true (time_limit_enabled); } +/* Basic test of mct_session_limits_serialize() on session limits. */ +static void +test_session_limits_serialize (void) +{ + g_auto(MctSessionLimitsBuilder) builder = MCT_SESSION_LIMITS_BUILDER_INIT (); + g_autoptr(MctSessionLimits) limits = NULL; + g_autoptr(GVariant) serialized = NULL; + + /* Use an empty #MctSessionLimits. */ + limits = mct_session_limits_builder_end (&builder); + + /* We can’t assert anything about the serialisation format, since it’s opaque. */ + serialized = mct_session_limits_serialize (limits); + g_assert_nonnull (serialized); +} + +/* Basic test of mct_session_limits_deserialize() on various current and historic + * serialised app filter variants. */ +static void +test_session_limits_deserialize (void) +{ + /* These are all opaque. Older versions should be kept around to test + * backwards compatibility. */ + const gchar *valid_session_limits[] = + { + "@a{sv} {}", + "{ 'LimitType': <@u 0> }", + "{ 'LimitType': <@u 1>, 'DailySchedule': <(@u 0, @u 100)> }", + "{ 'DailySchedule': <(@u 0, @u 100)> }", + }; + + for (gsize i = 0; i < G_N_ELEMENTS (valid_session_limits); i++) + { + g_autoptr(GVariant) serialized = NULL; + g_autoptr(MctSessionLimits) limits = NULL; + g_autoptr(GError) local_error = NULL; + + g_test_message ("%" G_GSIZE_FORMAT ": %s", i, valid_session_limits[i]); + + serialized = g_variant_parse (NULL, valid_session_limits[i], NULL, NULL, NULL); + g_assert (serialized != NULL); + + limits = mct_session_limits_deserialize (serialized, 1, &local_error); + g_assert_no_error (local_error); + g_assert_nonnull (limits); + } +} + +/* Test of mct_session_limits_deserialize() on various invalid variants. */ +static void +test_session_limits_deserialize_invalid (void) +{ + const gchar *invalid_session_limits[] = + { + "false", + "()", + "{ 'LimitType': <@u 100> }", + "{ 'DailySchedule': <(@u 100, @u 0)> }", + "{ 'DailySchedule': <(@u 0, @u 4294967295)> }", + }; + + for (gsize i = 0; i < G_N_ELEMENTS (invalid_session_limits); i++) + { + g_autoptr(GVariant) serialized = NULL; + g_autoptr(MctSessionLimits) limits = NULL; + g_autoptr(GError) local_error = NULL; + + g_test_message ("%" G_GSIZE_FORMAT ": %s", i, invalid_session_limits[i]); + + serialized = g_variant_parse (NULL, invalid_session_limits[i], NULL, NULL, NULL); + g_assert (serialized != NULL); + + limits = mct_session_limits_deserialize (serialized, 1, &local_error); + g_assert_error (local_error, MCT_MANAGER_ERROR, MCT_MANAGER_ERROR_INVALID_DATA); + g_assert_null (limits); + } +} + /* Fixture for tests which use an #MctSessionLimitsBuilder. The builder can * either be heap- or stack-allocated. @builder will always be a valid pointer * to it. @@ -1130,6 +1208,10 @@ main (int argc, g_test_add_func ("/session-limits/check-time-remaining/invalid-time", test_session_limits_check_time_remaining_invalid_time); + g_test_add_func ("/session-limits/serialize", test_session_limits_serialize); + g_test_add_func ("/session-limits/deserialize", test_session_limits_deserialize); + g_test_add_func ("/session-limits/deserialize/invalid", test_session_limits_deserialize_invalid); + g_test_add ("/session-limits/builder/stack/non-empty", BuilderFixture, NULL, builder_set_up_stack, test_session_limits_builder_non_empty, builder_tear_down_stack);