diff options
author | Matt Ranostay <matt.ranostay@konsulko.com> | 2019-06-14 17:56:41 -0700 |
---|---|---|
committer | Matt Ranostay <matt.ranostay@konsulko.com> | 2019-06-19 08:53:00 -0700 |
commit | 30b84ed96f8fc03fc5a73bbe97a86d76899ab8cc (patch) | |
tree | 3967da514a77ba4f1c4e186111ef44bc1197d2f3 | |
parent | a9584f072945bfda5d7741d43cb870dcb291fd70 (diff) |
binding: bluetooth-map: add message access support
Add support for retrieving messages, and listing handles of
messages located in folders.
SPEC-2512
Change-Id: Ia345488b51f5cac1e1c2fd508305d7ffda794251
Signed-off-by: Matt Ranostay <matt.ranostay@konsulko.com>
-rw-r--r-- | README.md | 67 | ||||
-rw-r--r-- | binding/CMakeLists.txt | 3 | ||||
-rw-r--r-- | binding/bluetooth-map-api.c | 209 | ||||
-rw-r--r-- | binding/bluetooth-map-api.h | 8 | ||||
-rw-r--r-- | binding/bluetooth-map-bluez.c | 6 | ||||
-rw-r--r-- | binding/bluetooth-map-common.h | 1 | ||||
-rw-r--r-- | binding/bluetooth-map-util.c | 1029 |
7 files changed, 1312 insertions, 11 deletions
@@ -7,14 +7,16 @@ to enable message notifications from SMS/email/etc. ### Important Notes -Message composition isn't available on iOS devices due to respective security polices, and in -turn isn't supported in Apple's implementation of the (MAP) Message Access Profile. +Message composition and message access isn't available on iOS devices due to respective security +polices, and in turn isn't supported in Apple's implementation of the (MAP) Message Access Profile. ## Verbs | Name | Description | JSON Response | |-------------------|------------------------------------------|-------------------------------------------| | compose | send message | see **compose verb** section | +| message | display message | see **message verb** section | +| list_messages | list messages in folder | see **list_messages** section | | subscribe | subscribe to MAP service events | *Request:* {"value": "notification"} | | unsubscribe | unsubscribe to MAP service events | *Request:* {"value": "notification"} | @@ -36,6 +38,67 @@ Send a message (if supported) via MAP profile: } </pre> +### message verb + +Request a message via the respective handle: + +<pre> +{ + "handle": "message288230376151711769" +} +</pre> + +Response: + +<pre> +{ + "response": { + "status": "UNREAD", + "type": "SMS_GSM", + "folder": "telecom/msg/INBOX", + "sender": { + "tel": "+13605551212" + }, + "message": "Meet at Victor 23 at 6p?" +}, + +</pre> + +### list_messages verb + +Request a folder listing of messages: + +<pre> +{ + "folder":"INBOX" +} +</pre> + +Response which the message handle as the key and includes its properties: + +<pre> +{ + "response": { + "message288230376151711769": { + "folder": "/telecom/msg/INBOX", + "subject": "Meet at Victor 23 at 6p?", + "timestamp": "20190614T093341", + "sender": "", + "senderaddress": "+13605551212", + "recipient": "", + "recipientaddress": "", + "type": "sms-gsm", + "size": 24, + "status": "complete", + "priority": false, + "read": false, + "sent": false, + "protected": false + }, + ... +}, +</pre> + ### notification event <pre> diff --git a/binding/CMakeLists.txt b/binding/CMakeLists.txt index 97a28df..7012e33 100644 --- a/binding/CMakeLists.txt +++ b/binding/CMakeLists.txt @@ -23,7 +23,8 @@ PROJECT_TARGET_ADD(bluetooth-map-binding) # Define project Targets add_library(bluetooth-map-binding MODULE bluetooth-map-api.c bluetooth-map-bluez.c - bluetooth-map-bmessage.c) + bluetooth-map-bmessage.c + bluetooth-map-util.c) # Binder exposes a unique public entry point SET_TARGET_PROPERTIES(${TARGET_NAME} PROPERTIES diff --git a/binding/bluetooth-map-api.c b/binding/bluetooth-map-api.c index a52df61..3d75bf4 100644 --- a/binding/bluetooth-map-api.c +++ b/binding/bluetooth-map-api.c @@ -339,7 +339,7 @@ static void compose(afb_req_t request) g_file_set_contents(name, message->str, -1, NULL); g_string_free(message, TRUE); - params = g_variant_new("(&s)", "telecom/msg/OUTBOX"); + params = g_variant_new("(&s)", "/telecom/msg/OUTBOX"); reply = bluez_call(ns, BLUEZ_AT_MESSAGEACCESS, session, "SetFolder", params, NULL); if (reply) g_variant_unref(reply); @@ -375,6 +375,158 @@ err_msg_invalid: g_free(session); } + +static void get_message_callback(void *user_data, + GVariant *result, GError **error) +{ + struct call_work *cw = user_data; + struct map_state *ns = cw->ns; + + bluez_decode_call_error(ns, + cw->access_type, cw->type_arg, cw->method, + error); + + if (error && *error) { + afb_req_fail_f(cw->request, "failed", "OBEX error: %s", + (*error)->message); + goto out_free; + } + + if (!result) { + goto out_free; + } + + g_variant_unref(result); + + return; + +out_free: + afb_req_unref(cw->request); + call_work_destroy(cw); +} + +static void message(afb_req_t request) +{ + struct map_state *ns = map_get_userdata(request); + GError *error = NULL; + GVariant *params; + const char *handle; + gchar *path, *filename; + struct call_work *cw; + + handle = afb_req_value(request, "handle"); + if (!handle) { + afb_req_fail_f(request, "failed", "no handle value passed"); + return; + } + + call_work_lock(ns); + if (!ns || !ns->session_path) { + afb_req_fail_f(request, "failed", "no valid OBEX session"); + call_work_unlock(ns); + return; + } + path = g_strconcat(ns->session_path, "/", handle, NULL); + call_work_unlock(ns); + + cw = call_work_create(ns, BLUEZ_AT_MESSAGE, NULL, + "get_message", "Get", &error); + if (!cw) { + afb_req_fail_f(request, "failed", "can't queue work %s", + error->message); + g_error_free(error); + goto err_queue_free; + } + + cw->request = request; + afb_req_addref(request); + + filename = g_strconcat("/tmp/obex-message-", handle, NULL); + params = g_variant_new("(&sb)", filename, g_variant_new_boolean(FALSE)); + cw->cpw = bluez_call_async(ns, BLUEZ_AT_MESSAGE, path, + "Get", params, &error, + get_message_callback, cw); + g_free(filename); + + if (!cw->cpw) { + afb_req_fail_f(request, "failed", "connection error %s", + error->message); + call_work_destroy(cw); + g_error_free(error); + /* fall-thru */ + } + +err_queue_free: + g_free(path); +} + +static void list_msgs(afb_req_t request) +{ + struct map_state *ns = map_get_userdata(request); + GVariant *params, *reply; + GVariantIter *iter = NULL, *iter2 = NULL; + const char *folder; + const gchar *path = NULL; + json_object *jresp; + gchar *session; + + folder = afb_req_value(request, "folder"); + if (!folder) { + afb_req_fail_f(request, "failed", "no folder value passed"); + return; + } + + call_work_lock(ns); + session = g_strdup(ns->session_path); + + params = g_variant_new("(&s)", "/telecom/msg"); + reply = bluez_call(ns, BLUEZ_AT_MESSAGEACCESS, session, "SetFolder", params, NULL); + if (!reply) { + afb_req_fail_f(request, "failed", "cannot switch to telecom/msg"); + goto out_free; + } + g_variant_unref(reply); + + params = g_variant_new("(&sa{sv})", folder, NULL); + reply = bluez_call(ns, BLUEZ_AT_MESSAGEACCESS, session, "ListMessages", params, NULL); + if (!reply) { + afb_req_fail_f(request, "failed", + "Cannot list messages in telecom/msg/%s", folder); + goto out_free; + } + + jresp = json_object_new_object(); + + g_variant_get(reply, "(a{oa{sv}})", &iter); + while (g_variant_iter_loop(iter, "{oa{sv}}", &path, &iter2)) { + const char *key = NULL; + GVariant *val = NULL; + json_object *msg = json_object_new_object(); + + while (g_variant_iter_loop(iter2, "{sv}", &key, &val)) { + GError *error = NULL; + gboolean is_config, ret; + + ret = message_property_dbus2json(msg, key, val, + &is_config, &error); + g_variant_unref(val); + if (!ret) { + AFB_DEBUG("%s property %s - %s", + path, + key, error->message); + g_clear_error(&error); + } + } + json_object_object_add(jresp, path + strlen(session) + 1, msg); + } + + afb_req_success_f(request, jresp, "Bluetooth MAP folder listing"); + +out_free: + call_work_unlock(ns); + g_free(session); +} + static void subscribe(afb_req_t request) { map_subscribe_unsubscribe(request, FALSE); @@ -423,6 +575,35 @@ static gboolean map_notification_check(GVariant *var) return FALSE; } +static void map_message_response(struct map_state *ns, gchar *filename) +{ + struct call_work *cw; + json_object *jresp; + gchar *buf; + + if (!g_file_get_contents(filename, &buf, NULL, NULL)) + return; + + jresp = bmessage_parse(buf); + + call_work_lock(ns); + + cw = call_work_lookup_unlocked(ns, BLUEZ_AT_MESSAGE, NULL, "get_message"); + if (!cw) { + if (jresp) + json_object_put(jresp); + return; + } + + + afb_req_success_f(cw->request, jresp, "Bluetooth MAP message result"); + + call_work_destroy_unlocked(cw); + + call_work_unlock(ns); + g_free(buf); +} + static void bluez_map_signal_callback( GDBusConnection *connection, const gchar *sender_name, @@ -477,14 +658,21 @@ static void bluez_map_signal_callback( while (g_variant_iter_next(array1, "{&sv}", &name, &val)) { struct xfer_tuple *data; + gchar *filename; if (g_strcmp0(name, "Filename")) continue; call_work_lock(ns); data = g_try_malloc0(sizeof(*data)); - data->type = XFER_NOTIFICATION; - data->value = g_strdup(g_variant_get_string(val, NULL)); + filename = g_strdup(g_variant_get_string(val, NULL)); + + // TODO: figure a better way that isn't based on name + if (strstr(filename, "obex-message-")) + data->type = XFER_MESSAGE; + else + data->type = XFER_NOTIFICATION; + data->value = filename; g_hash_table_insert(ns->xfer_queue, g_strdup(path), data); call_work_unlock(ns); break; @@ -538,6 +726,13 @@ static void bluez_map_signal_callback( afb_req_unref(request); g_free(val); break; + } else if (val->type == XFER_MESSAGE) { + if (!g_strcmp0(status, "complete")) + map_message_response(ns, (gchar *) val->value); + + g_free(val->value); + g_free(val); + break; } } } @@ -830,9 +1025,11 @@ static void onevent(afb_api_t api, const char *event, struct json_object *object } static const afb_verb_t binding_verbs[] = { - { .verb = "compose", .callback = compose, .info = "Compose message" }, - { .verb = "subscribe", .callback = subscribe, .info = "Subscribe to events" }, - { .verb = "unsubscribe",.callback = unsubscribe,.info = "Unsubscribe to events" }, + { .verb = "compose", .callback = compose, .info = "Compose message" }, + { .verb = "message", .callback = message, .info = "Retrieve message" }, + { .verb = "list_messages", .callback = list_msgs, .info = "List messages" }, + { .verb = "subscribe", .callback = subscribe, .info = "Subscribe to events" }, + { .verb = "unsubscribe", .callback = unsubscribe, .info = "Unsubscribe to events" }, {} }; diff --git a/binding/bluetooth-map-api.h b/binding/bluetooth-map-api.h index d5b20a1..5e373ba 100644 --- a/binding/bluetooth-map-api.h +++ b/binding/bluetooth-map-api.h @@ -116,6 +116,14 @@ static inline gboolean transfer_property_dbus2json(json_object *jprop, jprop, key, var, is_config, error); } +static inline gboolean message_property_dbus2json(json_object *jprop, + const gchar *key, GVariant *var, gboolean *is_config, + GError **error) +{ + return bluez_property_dbus2json(BLUEZ_AT_MESSAGE, + jprop, key, var, is_config, error); +} + static inline json_object *object_properties(struct map_state *ns, GError **error) { diff --git a/binding/bluetooth-map-bluez.c b/binding/bluetooth-map-bluez.c index f957f5c..4f1b850 100644 --- a/binding/bluetooth-map-bluez.c +++ b/binding/bluetooth-map-bluez.c @@ -38,8 +38,6 @@ #include "bluetooth-map-api.h" #include "bluetooth-map-common.h" -G_DEFINE_QUARK(bluetooth-map-error-quark, nb_error); - static const struct property_info session_props[] = { { .name = "Source", .fmt = "s", }, { .name = "Destination", .fmt = "s", }, @@ -257,6 +255,7 @@ bluez_call_async(struct map_state *ns, if (!type_arg && (!strcmp(access_type, BLUEZ_AT_SESSION) || !strcmp(access_type, BLUEZ_AT_TRANSFER) || + !strcmp(access_type, BLUEZ_AT_MESSAGE) || !strcmp(access_type, BLUEZ_AT_MESSAGEACCESS))) { g_set_error(error, NB_ERROR, NB_ERROR_MISSING_ARGUMENT, "missing %s argument", @@ -270,6 +269,9 @@ bluez_call_async(struct map_state *ns, } else if (!strcmp(access_type, BLUEZ_AT_TRANSFER)) { path = type_arg; interface = BLUEZ_OBEX_TRANSFER_INTERFACE; + } else if (!strcmp(access_type, BLUEZ_AT_MESSAGE)) { + path = type_arg; + interface = BLUEZ_OBEX_MESSAGE_INTERFACE; } else if (!strcmp(access_type, BLUEZ_AT_MESSAGEACCESS)) { path = type_arg; interface = BLUEZ_OBEX_MESSAGEACCESS_INTERFACE; diff --git a/binding/bluetooth-map-common.h b/binding/bluetooth-map-common.h index 8b2f469..f154c91 100644 --- a/binding/bluetooth-map-common.h +++ b/binding/bluetooth-map-common.h @@ -60,6 +60,7 @@ struct map_state { enum xfer_types { XFER_NOTIFICATION, XFER_SENTMSG, + XFER_MESSAGE, }; struct xfer_tuple { diff --git a/binding/bluetooth-map-util.c b/binding/bluetooth-map-util.c new file mode 100644 index 0000000..420c7d2 --- /dev/null +++ b/binding/bluetooth-map-util.c @@ -0,0 +1,1029 @@ +/* + * Copyright 2018 Konsulko Group + * Author: Pantelis Antoniou <pantelis.antoniou@konsulko.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#define _GNU_SOURCE +#include <stdio.h> +#include <string.h> +#include <stdlib.h> +#include <unistd.h> +#include <ctype.h> +#include <errno.h> +#include <pthread.h> +#include <semaphore.h> + +#include <glib.h> +#include <stdlib.h> +#include <gio/gio.h> +#include <glib-object.h> + +#include <json-c/json.h> + +#define AFB_BINDING_VERSION 3 +#include <afb/afb-binding.h> + +#include "bluetooth-map-api.h" +#include "bluetooth-map-common.h" + +G_DEFINE_QUARK(bluetooth-map-error-quark, nb_error) + +/* convert dbus key to lower case */ +gboolean auto_lowercase_keys = TRUE; + +void signal_init_done(struct init_data *id, int rc) +{ + g_mutex_lock(&id->mutex); + id->init_done = TRUE; + id->rc = rc; + g_cond_signal(&id->cond); + g_mutex_unlock(&id->mutex); +} + +int str2boolean(const char *value) +{ + if (!strcmp(value, "1") || !g_ascii_strcasecmp(value, "true") || + !g_ascii_strcasecmp(value, "on") || !g_ascii_strcasecmp(value, "enabled") || + !g_ascii_strcasecmp(value, "yes")) + return TRUE; + if (!strcmp(value, "0") || !g_ascii_strcasecmp(value, "false") || + !g_ascii_strcasecmp(value, "off") || !g_ascii_strcasecmp(value, "disabled") || + !g_ascii_strcasecmp(value, "no")) + return FALSE; + return -1; +} + +json_object *json_object_copy(json_object *jval) +{ + json_object *jobj; + size_t len; + int i; + + /* handle NULL */ + if (!jval) + return NULL; + + switch (json_object_get_type(jval)) { + case json_type_object: + jobj = json_object_new_object(); + json_object_object_foreach(jval, key, jval2) + json_object_object_add(jobj, key, + json_object_copy(jval2)); + + return jobj; + + case json_type_array: + jobj = json_object_new_array(); + len = json_object_array_length(jval); + for (i = 0; i < len; i++) + json_object_array_add(jobj, + json_object_copy( + json_object_array_get_idx(jval, i))); + return jobj; + + case json_type_null: + return NULL; + + case json_type_boolean: + return json_object_new_boolean( + json_object_get_boolean(jval)); + + case json_type_double: + return json_object_new_double( + json_object_get_double(jval)); + + case json_type_int: + return json_object_new_int64( + json_object_get_int64(jval)); + + case json_type_string: + return json_object_new_string( + json_object_get_string(jval)); + } + + g_assert(0); + /* should never happen */ + return NULL; +} + +gchar *key_dbus_to_json(const gchar *key, gboolean auto_lower) +{ + gchar *lower, *s; + + lower = g_strdup(key); + g_assert(lower); + + if (!auto_lower) + return lower; + + /* convert to lower case */ + for (s = lower; *s; s++) + *s = g_ascii_tolower(*s); + + return lower; +} + +json_object *simple_gvariant_to_json(GVariant *var, json_object *parent, + gboolean recurse) +{ + json_object *obj = NULL, *item; + gint32 i32; + gint64 i64; + guint32 ui32; + guint64 ui64; + GVariantIter iter; + GVariant *key, *value; + gchar *json_key; + gsize nitems; + gboolean is_dict; + + obj = NULL; + + /* AFB_DEBUG("g_variant_classify(var)=%c", g_variant_classify(var)); */ + + /* we only handle simple types */ + switch (g_variant_classify(var)) { + case G_VARIANT_CLASS_BOOLEAN: + obj = json_object_new_boolean(g_variant_get_boolean(var)); + break; + case G_VARIANT_CLASS_INT16: + obj = json_object_new_int(g_variant_get_int16(var)); + break; + case G_VARIANT_CLASS_INT32: + i32 = g_variant_get_int32(var); + obj = json_object_new_int(i32); + break; + case G_VARIANT_CLASS_INT64: + i64 = g_variant_get_int64(var); + if (i64 >= -(1L << 31) && i64 < (1L << 31)) + obj = json_object_new_int((int)i64); + else + obj = json_object_new_int64(i64); + break; + case G_VARIANT_CLASS_BYTE: + obj = json_object_new_int((int)g_variant_get_byte(var)); + break; + case G_VARIANT_CLASS_UINT16: + obj = json_object_new_int((int)g_variant_get_uint16(var)); + break; + case G_VARIANT_CLASS_UINT32: + ui32 = g_variant_get_uint32(var); + if (ui32 < (1U << 31)) + obj = json_object_new_int(ui32); + else + obj = json_object_new_int64(ui32); + break; + case G_VARIANT_CLASS_UINT64: + ui64 = g_variant_get_uint64(var); + if (ui64 < (1U << 31)) + obj = json_object_new_int((int)ui64); + else if (ui64 < (1LLU << 63)) + obj = json_object_new_int64(ui64); + else { + AFB_WARNING("U64 value %llu clamped to %llu", + (unsigned long long)ui64, + (unsigned long long)((1LLU << 63) - 1)); + obj = json_object_new_int64((1LLU << 63) - 1); + } + break; + case G_VARIANT_CLASS_DOUBLE: + obj = json_object_new_double(g_variant_get_double(var)); + break; + case G_VARIANT_CLASS_OBJECT_PATH: + case G_VARIANT_CLASS_STRING: + obj = json_object_new_string(g_variant_get_string(var, NULL)); + break; + + case G_VARIANT_CLASS_ARRAY: + + if (!recurse) + break; + + /* detect dictionaries which are arrays of dict entries */ + g_variant_iter_init(&iter, var); + + nitems = g_variant_iter_n_children(&iter); + /* remove completely empty arrays */ + if (nitems == 0) + break; + + is_dict = nitems > 0; + while (is_dict && (value = g_variant_iter_next_value(&iter))) { + is_dict = g_variant_classify(value) == G_VARIANT_CLASS_DICT_ENTRY; + g_variant_unref(value); + } + + if (is_dict) + obj = json_object_new_object(); + else + obj = json_object_new_array(); + + g_variant_iter_init(&iter, var); + while ((value = g_variant_iter_next_value(&iter))) { + + item = simple_gvariant_to_json(value, obj, TRUE); + if (!is_dict && item) + json_object_array_add(obj, item); + + g_variant_unref(value); + } + break; + + case G_VARIANT_CLASS_DICT_ENTRY: + + if (!recurse) + break; + + if (!parent) { + AFB_WARNING("#### dict new object without a parent"); + break; + } + + g_variant_iter_init(&iter, var); + while ((key = g_variant_iter_next_value(&iter))) { + + value = g_variant_iter_next_value(&iter); + if (!value) { + AFB_WARNING("Out of values with a key"); + g_variant_unref(key); + break; + } + + json_key = key_dbus_to_json( + g_variant_get_string(key, NULL), + auto_lowercase_keys); + + /* only handle dict values with string keys */ + if (g_variant_classify(key) == G_VARIANT_CLASS_STRING) { + item = simple_gvariant_to_json(value, obj, TRUE); + if (item) + json_object_object_add(parent, json_key, item); + + } else + AFB_WARNING("Can't handle non-string key"); + + g_free(json_key); + + g_variant_unref(value); + g_variant_unref(key); + } + break; + + case G_VARIANT_CLASS_VARIANT: + + /* NOTE: recurse allowed because we only allow a single encapsulated variant */ + + g_variant_iter_init(&iter, var); + nitems = g_variant_iter_n_children(&iter); + if (nitems != 1) { + AFB_WARNING("Can't handle variants with more than one children (%lu)", nitems); + break; + } + + while ((value = g_variant_iter_next_value(&iter))) { + obj = simple_gvariant_to_json(value, parent, TRUE); + g_variant_unref(value); + break; + } + break; + + default: + AFB_WARNING("############ class is %c", g_variant_classify(var)); + obj = NULL; + break; + } + + return obj; +} + +gchar *property_name_dbus2json(const struct property_info *pi, + gboolean is_config) +{ + gchar *json_name; + gchar *cfgname; + + if (pi->json_name) + json_name = g_strdup(pi->json_name); + else + json_name = key_dbus_to_json(pi->name, auto_lowercase_keys); + + if (!json_name) + return NULL; + + if (!is_config) + return json_name; + + cfgname = g_strdup_printf("%s.%configuration", + json_name, + auto_lowercase_keys ? 'c' : 'C'); + g_free(json_name); + return cfgname; +} + +json_object *property_dbus2json( + const struct property_info **pip, + const gchar *key, GVariant *var, + gboolean *is_config) +{ + const struct property_info *pi = *pip, *pi2, *pi_sub; + GVariantIter iter, iter2; + json_object *obj = NULL, *obji; + const char *fmt; + GVariant *value, *dict_value, *dict_key; + const gchar *sub_key; + gchar *json_key; + gboolean is_subconfig; + + if (key) { + pi = property_by_dbus_name(pi, key, is_config); + if (!pi) + return NULL; + *pip = pi; + } + + fmt = pi->fmt; + + obj = simple_gvariant_to_json(var, NULL, FALSE); + if (obj) { + /* TODO check fmt for matching type */ + return obj; + } + + switch (*fmt) { + case 'a': /* array */ + obj = json_object_new_array(); + + g_variant_iter_init(&iter, var); + while ((value = g_variant_iter_next_value(&iter))) { + pi2 = pi; + obji = property_dbus2json(&pi2, NULL, value, + &is_subconfig); + if (obji) + json_object_array_add(obj, obji); + + g_variant_unref(value); + } + break; + case '{': + /* we only support {sX} */ + + /* there must be a sub property entry */ + g_assert(pi->sub); + + obj = json_object_new_object(); + + g_variant_iter_init(&iter, var); + while ((value = g_variant_iter_next_value(&iter))) { + + if (g_variant_classify(value) != G_VARIANT_CLASS_DICT_ENTRY) { + AFB_WARNING("Expecting dict got '%c'", g_variant_classify(value)); + g_variant_unref(value); + break; + } + + g_variant_iter_init(&iter2, value); + while ((dict_key = g_variant_iter_next_value(&iter2))) { + if (g_variant_classify(dict_key) != G_VARIANT_CLASS_STRING) { + AFB_WARNING("Can't handle non-string dict keys '%c'", + g_variant_classify(dict_key)); + g_variant_unref(dict_key); + g_variant_unref(value); + continue; + } + + dict_value = g_variant_iter_next_value(&iter2); + if (!dict_value) { + AFB_WARNING("Out of values with a dict_key"); + g_variant_unref(dict_key); + g_variant_unref(value); + break; + } + + sub_key = g_variant_get_string(dict_key, NULL); + + pi_sub = pi->sub; + while (pi_sub->name) { + if (!g_strcmp0(sub_key, pi_sub->name)) + break; + pi_sub++; + } + + if (pi_sub->name) { + pi2 = pi_sub; + obji = property_dbus2json(&pi2, + sub_key, dict_value, + &is_subconfig); + if (obji) { + json_key = property_name_dbus2json(pi2, FALSE); + json_object_object_add(obj, json_key, obji); + g_free(json_key); + } + } else + AFB_INFO("Unhandled %s/%s property", key, sub_key); + + g_variant_unref(dict_value); + g_variant_unref(dict_key); + } + + g_variant_unref(value); + } + + break; + } + + if (!obj) + AFB_INFO("# %s not a type we can handle", key ? key : "<NULL>"); + + return obj; +} + +const struct property_info *property_by_dbus_name( + const struct property_info *pi, + const gchar *dbus_name, + gboolean *is_config) +{ + const struct property_info *pit; + const gchar *suffix; + gchar *tmpname; + size_t len; + + /* direct match first */ + pit = pi; + while (pit->name) { + if (!g_strcmp0(dbus_name, pit->name)) { + if (is_config) + *is_config = FALSE; + return pit; + } + pit++; + } + + /* try to see if a matching config property exists */ + suffix = strrchr(dbus_name, '.'); + if (!suffix || g_ascii_strcasecmp(suffix, ".Configuration")) + return NULL; + + /* it's a (possible) .config property */ + len = suffix - dbus_name; + tmpname = alloca(len + 1); + memcpy(tmpname, dbus_name, len); + tmpname[len] = '\0'; + + /* match with config */ + pit = pi; + while (pit->name) { + if (!(pit->flags & PI_CONFIG)) { + pit++; + continue; + } + if (!g_strcmp0(tmpname, pit->name)) { + if (is_config) + *is_config = TRUE; + return pit; + } + pit++; + } + + return NULL; +} + +const struct property_info *property_by_json_name( + const struct property_info *pi, + const gchar *json_name, + gboolean *is_config) +{ + const struct property_info *pit; + gchar *this_json_name; + const gchar *suffix; + gchar *tmpname; + size_t len; + + /* direct match */ + pit = pi; + while (pit->name) { + this_json_name = property_name_dbus2json(pit, FALSE); + if (!g_strcmp0(this_json_name, json_name)) { + g_free(this_json_name); + if (is_config) + *is_config = FALSE; + return pit; + } + g_free(this_json_name); + pit++; + } + + /* try to see if a matching config property exists */ + suffix = strrchr(json_name, '.'); + if (!suffix || g_ascii_strcasecmp(suffix, ".configuration")) + return NULL; + + /* it's a (possible) .config property */ + len = suffix - json_name; + tmpname = alloca(len + 1); + memcpy(tmpname, json_name, len); + tmpname[len] = '\0'; + + /* match with config */ + pit = pi; + while (pit->name) { + if (!(pit->flags & PI_CONFIG)) { + pit++; + continue; + } + this_json_name = property_name_dbus2json(pit, FALSE); + if (!g_strcmp0(this_json_name, tmpname)) { + g_free(this_json_name); + if (is_config) + *is_config = TRUE; + return pit; + } + g_free(this_json_name); + pit++; + } + + return NULL; +} + +const struct property_info *property_by_name( + const struct property_info *pi, + gboolean is_json_name, const gchar *name, + gboolean *is_config) +{ + return is_json_name ? + property_by_json_name(pi, name, is_config) : + property_by_dbus_name(pi, name, is_config); +} + +gchar *property_get_json_name(const struct property_info *pi, + const gchar *name) +{ + gboolean is_config; + + pi = property_by_dbus_name(pi, name, &is_config); + if (!pi) + return NULL; + return property_name_dbus2json(pi, is_config); +} + +gchar *configuration_dbus_name(const gchar *dbus_name) +{ + return g_strdup_printf("%s.Configuration", dbus_name); +} + +gchar *configuration_json_name(const gchar *json_name) +{ + return g_strdup_printf("%s.configuration", json_name); +} + +gchar *property_get_name(const struct property_info *pi, + const gchar *json_name) +{ + gboolean is_config; + + pi = property_by_json_name(pi, json_name, &is_config); + if (!pi) + return NULL; + + return !is_config ? g_strdup(pi->name) : + configuration_dbus_name(pi->name); +} + +gboolean root_property_dbus2json( + json_object *jparent, + const struct property_info *pi, + const gchar *key, GVariant *var, + gboolean *is_config) +{ + json_object *obj; + gchar *json_key; + + obj = property_dbus2json(&pi, key, var, is_config); + if (!obj) + return FALSE; + + switch (json_object_get_type(jparent)) { + case json_type_object: + json_key = property_name_dbus2json(pi, *is_config); + json_object_object_add(jparent, json_key, obj); + g_free(json_key); + break; + case json_type_array: + json_object_array_add(jparent, obj); + break; + default: + json_object_put(obj); + return FALSE; + } + + return TRUE; +} + +static const GVariantType *type_from_fmt(const char *fmt) +{ + switch (*fmt) { + case 'b': /* gboolean */ + return G_VARIANT_TYPE_BOOLEAN; + case 'y': /* guchar */ + return G_VARIANT_TYPE_BYTE; + case 'n': /* gint16 */ + return G_VARIANT_TYPE_INT16; + case 'q': /* guint16 */ + return G_VARIANT_TYPE_UINT16; + case 'h': + return G_VARIANT_TYPE_HANDLE; + case 'i': /* gint32 */ + return G_VARIANT_TYPE_INT32; + case 'u': /* guint32 */ + return G_VARIANT_TYPE_UINT32; + case 'x': /* gint64 */ + return G_VARIANT_TYPE_INT64; + case 't': /* gint64 */ + return G_VARIANT_TYPE_UINT64; + case 'd': /* double */ + return G_VARIANT_TYPE_DOUBLE; + case 's': /* string */ + return G_VARIANT_TYPE_STRING; + case 'o': /* object */ + return G_VARIANT_TYPE_OBJECT_PATH; + case 'g': /* signature */ + return G_VARIANT_TYPE_SIGNATURE; + case 'v': /* variant */ + return G_VARIANT_TYPE_VARIANT; + } + /* nothing complex */ + return NULL; +} + +GVariant *property_json_to_gvariant( + const struct property_info *pi, + const char *fmt, + const struct property_info *pi_parent, + json_object *jval, + GError **error) +{ + const struct property_info *pi_sub; + GVariant *arg, *item; + GVariantBuilder builder; + json_object *jitem; + json_bool b; + gchar *dbus_name; + int64_t i64; + double d; + const char *jvalstr, *str; + char c; + int i; + size_t len; + gboolean is_config; + + if (!fmt) + fmt = pi->fmt; + + if (!jval) { + g_set_error(error, NB_ERROR, NB_ERROR_BAD_PROPERTY, + "can't encode json NULL type"); + return NULL; + } + + jvalstr = json_object_to_json_string(jval); + + arg = NULL; + + b = FALSE; + i64 = 0; + d = 0.0; + str = NULL; + + /* conversion and type check */ + c = *fmt++; + if (c == 'a') { + if (!json_object_is_type(jval, json_type_array)) { + g_set_error(error, NB_ERROR, NB_ERROR_BAD_PROPERTY, + "Bad property \"%s\" (not an array)", + jvalstr); + return NULL; + } + + len = json_object_array_length(jval); + /* special case for empty array */ + if (!len) { + arg = g_variant_new_array(type_from_fmt(fmt), NULL, 0); + if (!arg) + g_set_error(error, NB_ERROR, NB_ERROR_BAD_PROPERTY, + "Can't create empty array on \"%s\"", + jvalstr); + return arg; + } + + g_variant_builder_init(&builder, G_VARIANT_TYPE_ARRAY); + for (i = 0; i < len; i++) { + jitem = json_object_array_get_idx(jval, i); + item = property_json_to_gvariant(pi, fmt, NULL, jitem, error); + if (!item) { + g_variant_builder_clear(&builder); + return NULL; + } + g_variant_builder_add_value(&builder, item); + } + + arg = g_variant_builder_end(&builder); + + if (!arg) + g_set_error(error, NB_ERROR, NB_ERROR_BAD_PROPERTY, + "Can't handle array on \"%s\"", + jvalstr); + + return arg; + } + if (c == '{') { + g_assert(pi->sub); + + c = *fmt++; + /* we only handle string keys */ + if (c != 's') { + g_set_error(error, NB_ERROR, NB_ERROR_BAD_PROPERTY, + "Can't handle non-string keys on \"%s\"", + jvalstr); + return NULL; + } + c = *fmt++; + + /* this is arguably wrong */ + g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}")); + pi_sub = pi->sub; + json_object_object_foreach(jval, key_o, jval_o) { + pi_sub = property_by_json_name(pi->sub, key_o, &is_config); + if (!pi_sub) { + g_set_error(error, NB_ERROR, NB_ERROR_BAD_PROPERTY, + "Unknown sub-property %s in \"%s\"", + key_o, json_object_to_json_string(jval_o)); + return NULL; + } + item = property_json_to_gvariant(pi_sub, NULL, pi, jval_o, error); + if (!item) + return NULL; + + dbus_name = property_get_name(pi->sub, key_o); + g_assert(dbus_name); /* can't fail; but check */ + + g_variant_builder_add(&builder, pi->fmt, dbus_name, item); + + g_free(dbus_name); + } + + arg = g_variant_builder_end(&builder); + + if (!arg) + g_set_error(error, NB_ERROR, NB_ERROR_BAD_PROPERTY, + "Can't handle object on \"%s\"", + jvalstr); + return arg; + } + + switch (c) { + case 'b': /* gboolean */ + if (!json_object_is_type(jval, json_type_boolean)) { + g_set_error(error, NB_ERROR, NB_ERROR_BAD_PROPERTY, + "Bad property \"%s\" (not a boolean)", + jvalstr); + return NULL; + } + b = json_object_get_boolean(jval); + break; + case 'y': /* guchar */ + case 'n': /* gint16 */ + case 'q': /* guint16 */ + case 'h': + case 'i': /* gint32 */ + case 'u': /* guint32 */ + case 'x': /* gint64 */ + case 't': /* gint64 */ + if (!json_object_is_type(jval, json_type_int)) { + g_set_error(error, NB_ERROR, NB_ERROR_BAD_PROPERTY, + "Bad property \"%s\" (not an integer)", + jvalstr); + return NULL; + } + /* note unsigned 64 bit values shall be truncated */ + i64 = json_object_get_int64(jval); + break; + + case 'd': /* double */ + if (!json_object_is_type(jval, json_type_double)) { + g_set_error(error, NB_ERROR, NB_ERROR_BAD_PROPERTY, + "Bad property \"%s\" (not a double)", + jvalstr); + return NULL; + } + d = json_object_get_double(jval); + break; + case 's': /* string */ + case 'o': /* object */ + case 'g': /* signature */ + if (!json_object_is_type(jval, json_type_string)) { + g_set_error(error, NB_ERROR, NB_ERROR_BAD_PROPERTY, + "Bad property \"%s\" (not a string)", + jvalstr); + return NULL; + } + str = json_object_get_string(jval); + break; + case 'v': /* variant */ + AFB_WARNING("Can't handle variant yet"); + break; + } + + /* build gvariant */ + switch (c) { + case 'b': /* gboolean */ + arg = g_variant_new_boolean(b); + break; + case 'y': /* guchar */ + if (i64 < 0 || i64 > 255) { + g_set_error(error, NB_ERROR, NB_ERROR_BAD_PROPERTY, + "Bad property %s (out of byte range)", + jvalstr); + return FALSE; + } + arg = g_variant_new_byte((guchar)i64); + break; + case 'n': /* gint16 */ + if (i64 < -(1LL << 15) || i64 >= (1LL << 15)) { + g_set_error(error, NB_ERROR, NB_ERROR_BAD_PROPERTY, + "Bad property %s (out of int16 range)", + jvalstr); + return FALSE; + } + arg = g_variant_new_int16((gint16)i64); + break; + case 'q': /* guint16 */ + if (i64 < 0 || i64 >= (1LL << 16)) { + g_set_error(error, NB_ERROR, NB_ERROR_BAD_PROPERTY, + "Bad property %s (out of uint16 range)", + jvalstr); + return FALSE; + } + arg = g_variant_new_uint16((guint16)i64); + break; + case 'h': + case 'i': /* gint32 */ + if (i64 < -(1LL << 31) || i64 >= (1LL << 31)) { + g_set_error(error, NB_ERROR, NB_ERROR_BAD_PROPERTY, + "Bad property %s (out of int32 range)", + jvalstr); + return FALSE; + } + arg = g_variant_new_int32((gint32)i64); + break; + case 'u': /* guint32 */ + if (i64 < 0 || i64 >= (1LL << 32)) { + g_set_error(error, NB_ERROR, NB_ERROR_BAD_PROPERTY, + "Bad property %s (out of uint32 range)", + jvalstr); + return FALSE; + } + arg = g_variant_new_uint32((guint32)i64); + break; + case 'x': /* gint64 */ + arg = g_variant_new_int64(i64); + break; + case 't': /* gint64 */ + arg = g_variant_new_uint64(i64); + break; + case 'd': /* double */ + arg = g_variant_new_double(d); + break; + case 's': /* string */ + arg = g_variant_new_string(str); + break; + case 'o': /* object */ + arg = g_variant_new_object_path(str); + break; + case 'g': /* signature */ + arg = g_variant_new_signature(str); + break; + case 'v': /* variant */ + AFB_WARNING("Can't handle variant yet"); + break; + } + + return arg; +} + +json_object *get_property_collect(json_object *jreqprop, json_object *jprop, + GError **error) +{ + size_t len; + int i; + json_object *jkey, *jval, *jobj = NULL, *jobjval; + const char *key; + + + /* printf("jreqprop=%s\n", json_object_to_json_string_ext(jreqprop, + JSON_C_TO_STRING_SPACED)); + printf("jprop=%s\n", json_object_to_json_string_ext(jprop, + JSON_C_TO_STRING_SPACED)); */ + + /* get is an array of strings (or an object for subtype */ + g_assert(json_object_get_type(jreqprop) == json_type_array); + + len = json_object_array_length(jreqprop); + if (len == 0) + return NULL; + + for (i = 0; i < len; i++) { + jkey = json_object_array_get_idx(jreqprop, i); + + /* string key */ + if (json_object_is_type(jkey, json_type_string)) { + key = json_object_get_string(jkey); + if (!json_object_object_get_ex(jprop, key, &jval)) { + g_set_error(error, + NB_ERROR, NB_ERROR_BAD_PROPERTY, + "can't find key %s", key); + json_object_put(jobj); + return NULL; + } + + if (!jobj) + jobj = json_object_new_object(); + + json_object_object_add(jobj, key, + json_object_copy(jval)); + + } else if (json_object_is_type(jkey, json_type_object)) { + /* recursing into an object */ + + json_object_object_foreach(jkey, key_o, jval_o) { + if (!json_object_object_get_ex(jprop, key_o, + &jval)) { + g_set_error(error, NB_ERROR, + NB_ERROR_BAD_PROPERTY, + "can't find key %s", key_o); + json_object_put(jobj); + return NULL; + } + + /* jval_o is on jreqprop */ + /* jval is on jprop */ + + jobjval = get_property_collect(jval_o, jval, + error); + + if (!jobjval && error && *error) { + json_object_put(jobj); + return NULL; + } + + if (jobjval) { + if (!jobj) + jobj = json_object_new_object(); + + json_object_object_add(jobj, key_o, jobjval); + } + } + } + } + + /* if (jobj) + printf("jobj=%s\n", json_object_to_json_string_ext(jobj, + JSON_C_TO_STRING_SPACED)); */ + + return jobj; +} + +json_object *get_named_property(const struct property_info *pi, + gboolean is_json_name, const char *name, json_object *jprop) +{ + json_object *jret = NULL; + gchar *json_name = NULL; + + if (!is_json_name) { + json_name = property_get_json_name(pi, name); + if (!json_name) + return NULL; + name = json_name; + } + + json_object_object_foreach(jprop, key, jval) { + if (!g_strcmp0(key, name)) { + jret = json_object_copy(jval); + break; + } + } + + g_free(json_name); + + return jret; +} |