diff --git a/src/calls-emergency-call-types.h b/src/calls-emergency-call-types.h index a0ccc40..d544fdb 100644 --- a/src/calls-emergency-call-types.h +++ b/src/calls-emergency-call-types.h @@ -25,7 +25,7 @@ typedef enum { /* See 3GPP TS 22.101 version 14.8.0 Release 14, Chapter 10.1 */ typedef enum { - CALLS_EMERGENCY_CALL_TYPE_UNKNOWN = 0, + CALLS_EMERGENCY_CALL_TYPE_NONE = 0, CALLS_EMERGENCY_CALL_TYPE_POLICE = (1 << 0), CALLS_EMERGENCY_CALL_TYPE_AMBULANCE = (1 << 1), CALLS_EMERGENCY_CALL_TYPE_FIRE_BRIGADE = (1 << 2), diff --git a/src/calls-service-providers.c b/src/calls-service-providers.c new file mode 100644 index 0000000..f687919 --- /dev/null +++ b/src/calls-service-providers.c @@ -0,0 +1,461 @@ +/* + * Copyright (C) 2025 The Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "calls-service-providers" + +#include "calls-emergency-call-types.h" +#include "calls-service-providers.h" + +#include +#include + +typedef enum { + PARSER_TOPLEVEL = 0, + PARSER_COUNTRY, + PARSER_EMERGENCY_NUMBERS, + PARSER_EMERGENCY_NUMBER, + PARSER_CALLEE, + PARSER_DONE, + PARSER_ERROR +} GsdParseContextState; + + +typedef struct { + GMarkupParseContext *ctx; + GHashTable *info; + char buffer[4096]; + + char *text_buffer; + GsdParseContextState state; + CallsEmergencyCallCountryData *current_data; + CallsEmergencyNumber *current_number; + CallsEmergencyCallTypeFlags current_flags; +} CallsParseContext; + + +typedef struct { + GAsyncResult *res; + GMainLoop *loop; +} GetChannelsSyncData; + + +static void +calls_parse_context_free (CallsParseContext *parse_context) +{ + g_markup_parse_context_free (parse_context->ctx); + g_clear_pointer (&parse_context->current_data, calls_emergency_call_country_data_free); + g_clear_pointer (&parse_context->current_number, calls_emergency_number_free); + g_clear_pointer (&parse_context->info, g_hash_table_unref); + g_clear_pointer (&parse_context->text_buffer, g_free); + + g_free (parse_context); +} + + +static void +parser_toplevel_start (CallsParseContext *parse_context, + const char *name, + const char **attribute_names, + const char **attribute_values) +{ + if (g_str_equal (name, "serviceproviders")) { + for (int i = 0; !gm_str_is_null_or_empty (attribute_names[i]); i++) { + if (g_str_equal (attribute_names[i], "format")) { + if (!g_str_equal (attribute_values[i], "2.0")) { + g_warning ("Mobile broadband provider database format '%s' not supported.", + attribute_values[i]); + parse_context->state = PARSER_ERROR; + break; + } + } + } + } else if (g_str_equal (name, "country")) { + parse_context->state = PARSER_COUNTRY; + + if (parse_context->current_data) { + g_warning ("Country '%s' not fully parsed", parse_context->current_data->country_code); + g_clear_pointer (&parse_context->current_data, calls_emergency_call_country_data_free); + } + + for (int i = 0; !gm_str_is_null_or_empty (attribute_names[i]); i++) { + if (g_str_equal (attribute_names[i], "code")) { + g_assert (parse_context->current_data == NULL); + parse_context->current_data = calls_emergency_call_country_data_new (attribute_values[i]); + break; + } + } + } +} + + +static void +parser_country_start (CallsParseContext *parse_context, + const char *name, + const char **attribute_names, + const char **attribute_values) +{ + if (g_str_equal (name, "emergency-numbers")) + parse_context->state = PARSER_EMERGENCY_NUMBERS; +} + + +static void +parser_emergency_numbers_start (CallsParseContext *parse_context, + const char *name, + const char **attribute_names, + const char **attribute_values) +{ + if (g_str_equal (name, "emergency-number")) { + for (int i = 0; !gm_str_is_null_or_empty (attribute_names[i]); i++) { + if (g_str_equal (attribute_names[i], "number")) { + + g_assert (parse_context->current_number == NULL); + parse_context->current_number = calls_emergency_number_new (attribute_values[i], + CALLS_EMERGENCY_CALL_TYPE_NONE); + + break; + } + } + parse_context->state = PARSER_EMERGENCY_NUMBER; + } +} + + +static CallsEmergencyCallTypeFlags +type_to_flag (const char *type) +{ + if (g_str_equal (type, "police")) { + return CALLS_EMERGENCY_CALL_TYPE_POLICE; + } else if (g_str_equal (type, "ambulance")) { + return CALLS_EMERGENCY_CALL_TYPE_AMBULANCE; + } else if (g_str_equal (type, "fire-brigade")) { + return CALLS_EMERGENCY_CALL_TYPE_FIRE_BRIGADE; + } + + return CALLS_EMERGENCY_CALL_TYPE_NONE; +} + + +static void +parser_emergency_number_start (CallsParseContext *parse_context, + const char *name, + const char **attribute_names, + const char **attribute_values) +{ + if (g_str_equal (name, "callee")) { + + g_assert (parse_context->current_flags == CALLS_EMERGENCY_CALL_TYPE_NONE); + for (int i = 0; !gm_str_is_null_or_empty (attribute_names[i]); i++) { + if (g_str_equal (attribute_names[i], "type")) { + parse_context->current_flags = type_to_flag (attribute_values[i]); + break; + } + } + + parse_context->state = PARSER_CALLEE; + } +} + + +static void +parser_start_element (GMarkupParseContext *context, + const char *element_name, + const char **attribute_names, + const char **attribute_values, + gpointer user_data, + GError **error) +{ + CallsParseContext *parse_context = user_data; + + g_clear_pointer (&parse_context->text_buffer, g_free); + + switch (parse_context->state) { + case PARSER_TOPLEVEL: + parser_toplevel_start (parse_context, element_name, attribute_names, attribute_values); + break; + case PARSER_COUNTRY: + parser_country_start (parse_context, element_name, attribute_names, attribute_values); + break; + case PARSER_EMERGENCY_NUMBERS: + parser_emergency_numbers_start (parse_context, element_name, attribute_names, attribute_values); + break; + case PARSER_EMERGENCY_NUMBER: + parser_emergency_number_start (parse_context, element_name, attribute_names, attribute_values); + break; + case PARSER_CALLEE: + break; + case PARSER_ERROR: + break; + case PARSER_DONE: + break; + default: + g_assert_not_reached (); + } +} + + +static void +parser_callee_end (CallsParseContext *parse_context, const char *name) +{ + if (g_str_equal (name, "callee")) { + parse_context->current_number->flags |= parse_context->current_flags; + parse_context->current_flags = CALLS_EMERGENCY_CALL_TYPE_NONE; + g_clear_pointer (&parse_context->text_buffer, g_free); + parse_context->state = PARSER_EMERGENCY_NUMBER; + } +} + + +static void +parser_emergency_number_end (CallsParseContext *parse_context, const char *name) +{ + if (g_str_equal (name, "emergency-number")) { + g_ptr_array_add (parse_context->current_data->numbers, + g_steal_pointer (&parse_context->current_number)); + g_clear_pointer (&parse_context->text_buffer, g_free); + parse_context->state = PARSER_EMERGENCY_NUMBERS; + } +} + + +static void +parser_emergency_numbers_end (CallsParseContext *parse_context, const char *name) +{ + if (g_str_equal (name, "emergency-numbers")) { + g_clear_pointer (&parse_context->text_buffer, g_free); + parse_context->state = PARSER_COUNTRY; + } +} + + +static void +parser_country_end (CallsParseContext *parse_context, const char *name) +{ + if (g_str_equal (name, "country")) { + /* Only add country if we have any emergency numbers */ + if (parse_context->current_data->numbers->len) { + g_hash_table_insert (parse_context->info, + parse_context->current_data->country_code, + parse_context->current_data); + parse_context->current_data = NULL; + } + g_clear_pointer (&parse_context->current_data, calls_emergency_call_country_data_free); + g_clear_pointer (&parse_context->text_buffer, g_free); + parse_context->state = PARSER_TOPLEVEL; + } +} + + +static void +parser_end_element (GMarkupParseContext *context, + const char *element_name, + gpointer user_data, + GError **error) +{ + CallsParseContext *parse_context = user_data; + + switch (parse_context->state) { + case PARSER_TOPLEVEL: + break; + case PARSER_COUNTRY: + parser_country_end (parse_context, element_name); + break; + case PARSER_EMERGENCY_NUMBERS: + parser_emergency_numbers_end (parse_context, element_name); + break; + case PARSER_EMERGENCY_NUMBER: + parser_emergency_number_end (parse_context, element_name); + break; + case PARSER_CALLEE: + parser_callee_end (parse_context, element_name); + case PARSER_ERROR: + break; + case PARSER_DONE: + break; + default: + g_assert_not_reached (); + } +} + + +static void +parser_text (GMarkupParseContext *context, + const char *text, + gsize text_len, + gpointer user_data, + GError **error) +{ + CallsParseContext *parse_context = user_data; + + g_free (parse_context->text_buffer); + parse_context->text_buffer = g_strdup (text); +} + + +static const GMarkupParser parser = { + .start_element = parser_start_element, + .end_element = parser_end_element, + .text = parser_text, + .passthrough = NULL, + .error = NULL, +}; + + +static void read_next_chunk (GInputStream *stream, GTask *task); + + +static void +on_stream_read_ready (GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + GInputStream *stream = G_INPUT_STREAM (source_object); + g_autoptr (GTask) task = G_TASK (user_data); + CallsParseContext *parse_context = g_task_get_task_data (task); + gssize len; + GError *error = NULL; + + len = g_input_stream_read_finish (stream, res, &error); + if (len == -1) { + g_prefix_error (&error, "Error reading service provider database: "); + g_task_return_error (task, error); + return; + } + + if (len == 0) { + g_task_return_pointer (task, + g_steal_pointer (&parse_context->info), + (GDestroyNotify)g_hash_table_unref); + return; + } + + if (!g_markup_parse_context_parse (parse_context->ctx, parse_context->buffer, len, &error)) { + g_prefix_error (&error, "Error parsing service provider database: "); + g_task_return_error (task, error); + return; + } + + read_next_chunk (stream, g_steal_pointer (&task)); +} + + +static void +read_next_chunk (GInputStream *stream, GTask *task) +{ + CallsParseContext *parse_context = g_task_get_task_data (task); + + g_input_stream_read_async (stream, + parse_context->buffer, + sizeof (parse_context->buffer), + G_PRIORITY_DEFAULT, + g_task_get_cancellable (task), + on_stream_read_ready, + task); +} + + +static void +on_file_read_ready (GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + g_autoptr (GTask) task = G_TASK (user_data); + g_autoptr (GFileInputStream) stream = NULL; + GError *error = NULL; + GFile *file = G_FILE (source_object); + + stream = g_file_read_finish (file, res, &error); + if (!stream) { + g_prefix_error (&error, "Error opening service provider database: "); + g_task_return_error (task, error); + return; + } + + read_next_chunk (G_INPUT_STREAM (stream), g_steal_pointer (&task)); +} + + +GHashTable * + +calls_service_providers_get_emergency_info_finish (GAsyncResult *res, + GError **error) +{ + g_assert (G_IS_TASK (res)); + g_assert (g_task_get_source_tag(G_TASK (res)) == calls_service_providers_get_emergency_info); + + return g_task_propagate_pointer (G_TASK (res), error); +} + + +void +calls_service_providers_get_emergency_info (const char *serviceproviders, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr (GFile) file = NULL; + g_autoptr (GTask) task = g_task_new (NULL, cancellable, callback, user_data); + CallsParseContext *parse_context = g_new0 (CallsParseContext, 1); + + g_assert (serviceproviders); + + parse_context->ctx = g_markup_parse_context_new (&parser, 0, parse_context, NULL); + parse_context->info = g_hash_table_new_full (g_str_hash, + g_str_equal, + NULL, + (GDestroyNotify) + calls_emergency_call_country_data_free); + + g_task_set_task_data (task, parse_context, (GDestroyNotify)calls_parse_context_free); + g_task_set_source_tag (task, calls_service_providers_get_emergency_info); + + file = g_file_new_for_path (serviceproviders); + g_file_read_async (file, G_PRIORITY_DEFAULT, + cancellable, + on_file_read_ready, + g_steal_pointer (&task)); +} + + +static void +on_get_emergency_info_ready (GObject *object, GAsyncResult *res, gpointer user_data) +{ + GetChannelsSyncData *data = user_data; + + g_assert (data->res == NULL); + data->res = g_object_ref (res); + g_main_loop_quit (data->loop); +} + + +GHashTable * +calls_service_providers_get_emergency_info_sync (const char *serviceproviders, + GError **error) +{ + GHashTable *info; + GetChannelsSyncData data; + g_autoptr (GMainContext) context = g_main_context_new (); + g_autoptr (GMainLoop) loop = NULL; + + g_main_context_push_thread_default (context); + loop = g_main_loop_new (context, FALSE); + + data = (GetChannelsSyncData) { + .loop = loop, + .res = NULL, + }; + + calls_service_providers_get_emergency_info (serviceproviders, + NULL, + on_get_emergency_info_ready, + &data); + g_main_loop_run (data.loop); + + info = calls_service_providers_get_emergency_info_finish (data.res, error); + + g_clear_object (&data.res); + g_main_context_pop_thread_default (context); + + return info; +} diff --git a/src/calls-service-providers.h b/src/calls-service-providers.h new file mode 100644 index 0000000..89e673f --- /dev/null +++ b/src/calls-service-providers.h @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2025 The Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +void calls_service_providers_get_emergency_info (const char *serviceproviders, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +GHashTable *calls_service_providers_get_emergency_info_finish (GAsyncResult *res, + GError **error); +GHashTable *calls_service_providers_get_emergency_info_sync (const char *serviceproviders, + GError **error) G_GNUC_WARN_UNUSED_RESULT; + + +G_END_DECLS diff --git a/src/meson.build b/src/meson.build index 2e26a46..d83af38 100644 --- a/src/meson.build +++ b/src/meson.build @@ -123,6 +123,7 @@ calls_sources = files([ 'calls-ringer.c', 'calls-ringer.h', 'calls-secret-store.c', 'calls-secret-store.h', 'calls-settings.c', 'calls-settings.h', + 'calls-service-providers.c', 'calls-service-providers.h', 'calls-ui-call-data.c', 'calls-ui-call-data.h', 'calls-ussd.c', 'calls-ussd.h', 'calls-util.c', 'calls-util.h', diff --git a/tests/data/serviceproviders.xml b/tests/data/serviceproviders.xml new file mode 100644 index 0000000..a705b23 --- /dev/null +++ b/tests/data/serviceproviders.xml @@ -0,0 +1,35 @@ + + + + + + Germany + + + + + + + + + + + + + Switzerland + + + + + + + + + + + + + + + + diff --git a/tests/meson.build b/tests/meson.build index 8a5bd9a..4988d99 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -19,6 +19,7 @@ test_env = [ test_cflags = [ '-DFOR_TESTING', '-Wno-error=deprecated-declarations', + '-DTEST_DATABASE="@0@"'.format(meson.current_source_dir() / 'data' / 'serviceproviders.xml'), ] test_link_args = [ @@ -52,6 +53,19 @@ t = executable('emergency-call-types', test_sources, ) test('emergency-call-types', t, env: test_env) +test_sources = [ 'test-service-providers.c' ] +t = executable('service-providers', test_sources, + c_args : test_cflags, + link_args: test_link_args, + pie: true, + link_with : [calls_vala, libcalls], + dependencies: calls_deps, + include_directories : [ + calls_includes, + ] + ) +test('service-providers', t, env: test_env) + test_sources = [ 'test-manager.c' ] t = executable('manager', test_sources, diff --git a/tests/test-service-providers.c b/tests/test-service-providers.c new file mode 100644 index 0000000..b4784e1 --- /dev/null +++ b/tests/test-service-providers.c @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2025 The Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0+ + * + * Author: Guido Günther + */ + +#include "calls-emergency-call-types.h" +#include "calls-service-providers.h" + +#include +#include + + +#define calls_assert_cmp_emergency_number(d, i, n, f) G_STMT_START { \ + CallsEmergencyNumber *_n = g_ptr_array_index (d->numbers, i); \ + if (!_n) { \ + g_autofree char *__msg = \ + g_strdup_printf ("Emergency number '%u' does not exist", i); \ + g_assertion_message (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, __msg); \ + } \ + if (!_n->number) { \ + g_autofree char *__msg = \ + g_strdup_printf ("Emergency number of element '%u' is NULL", i); \ + g_assertion_message (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, __msg); \ + } \ + if (!g_str_equal (_n->number, n)) { \ + g_autofree char *__msg = \ + g_strdup_printf ("Emergency number of element '%u' is '%s' not '%s'", i, _n->number, n); \ + g_assertion_message (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, __msg); \ + } \ + if (_n->flags != f) { \ + g_autofree char *__msg = \ + g_strdup_printf ("Emergency number of element '%u' has flags '0x%x'' not '0x%x'", i, _n->flags, f); \ + g_assertion_message (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, __msg); \ + } \ +} G_STMT_END + + +static gboolean +numbers_equal (gconstpointer a, gconstpointer b) +{ + const CallsEmergencyNumber *n_a = a; + const char *needle = b; + + return g_str_equal (n_a->number, needle); +} + + +static void +test_service_providers_parse_de (void) +{ + g_autoptr (GHashTable) info = NULL; + g_autoptr (GError) err = NULL; + CallsEmergencyCallCountryData *data; + CallsEmergencyNumber *number; + guint index; + + info = g_hash_table_new_full (g_str_hash, + g_str_equal, + NULL, + (GDestroyNotify) calls_emergency_call_country_data_free); + + info = calls_service_providers_get_emergency_info_sync (TEST_DATABASE, &err); + + g_assert_no_error (err); + g_assert_nonnull (info); + + data = g_hash_table_lookup (info, "xx"); + g_assert_nonnull (data); + g_assert_cmpint (data->numbers->len, ==, 3); + + g_assert_true (g_ptr_array_find_with_equal_func (data->numbers, "114", numbers_equal, &index)); + number = g_ptr_array_index (data->numbers, index); + g_assert_nonnull (number); + calls_assert_cmp_emergency_number(data, index, "114", CALLS_EMERGENCY_CALL_TYPE_AMBULANCE); + + g_assert_true (g_ptr_array_find_with_equal_func (data->numbers, "117", numbers_equal, &index)); + number = g_ptr_array_index (data->numbers, index); + g_assert_nonnull (number); + calls_assert_cmp_emergency_number(data, index, "117", CALLS_EMERGENCY_CALL_TYPE_POLICE); + + g_assert_true (g_ptr_array_find_with_equal_func (data->numbers, "118", numbers_equal, &index)); + number = g_ptr_array_index (data->numbers, index); + g_assert_nonnull (number); + calls_assert_cmp_emergency_number(data, index, "118", CALLS_EMERGENCY_CALL_TYPE_FIRE_BRIGADE); + + data = g_hash_table_lookup (info, "yy"); + g_assert_nonnull (data); + g_assert_cmpint (data->numbers->len, ==, 1); + g_assert_true (g_ptr_array_find_with_equal_func (data->numbers, "112", numbers_equal, &index)); + number = g_ptr_array_index (data->numbers, index); + g_assert_nonnull (number); + calls_assert_cmp_emergency_number(data, index, "112", (CALLS_EMERGENCY_CALL_TYPE_POLICE | + CALLS_EMERGENCY_CALL_TYPE_FIRE_BRIGADE | + CALLS_EMERGENCY_CALL_TYPE_AMBULANCE)); + + data = g_hash_table_lookup (info, "zz"); + g_assert_null (data); +} + + +gint +main (gint argc, gchar *argv[]) +{ + g_test_init (&argc, &argv, NULL); + + g_test_add_func ("/calls/service-providers/parse", test_service_providers_parse_de); + + return g_test_run (); +}