message-params: Allow parameter strings to contain escaped curly braces
authorGeorg Chini <georg@chini.tk>
Tue, 14 Jan 2020 12:24:16 +0000 (13:24 +0100)
committerTanu Kaskinen <tanuk@iki.fi>
Thu, 3 Dec 2020 14:41:39 +0000 (14:41 +0000)
The patch adds the possibility to escape curly braces within parameter strings
and introduces several new functions that can be used for writing parameters.

For writing, the structure pa_message_params, which is a wrapper for pa_strbuf
has been created. Following new write functions are available:

pa_message_params_new() - creates a new pa_message_params structure
pa_message_params_free() - frees a pa_message_params structure
pa_message_param_to_string_free() - converts a pa_message_param to string and
frees the structure
pa_message_params_begin_list() - starts a list
pa_message_params_end_list() - ends a list
pa_message_params_write_string() - writes a string to a pa_message_params structure
pa_message_params_write_raw() - writes a raw string to a pa_message_params structure

For string parameters that contain curly braces or backslashes, those characters
will be escaped when using pa_message_params_write_string(), while write_raw() will
put the string into the buffer without any changes.

For reading, pa_message_params_read_string() reverts the changes that
pa_message_params_write_string() might have introduced.

The patch also adds more restrictions on the object path name. Now only
alphanumeric characters and one of "_", ".", "-" and "/" are allowed.
The path name may not end with a / or contain a double slash. If the user
specifies a trailing / when sending a message, it will be silently removed.

Part-of: <https://gitlab.freedesktop.org/pulseaudio/pulseaudio/-/merge_requests/51>

doc/messaging_api.txt
src/Makefile.am
src/map-file
src/meson.build
src/pulse/message-params.c
src/pulse/message-params.h
src/pulsecore/core.c
src/pulsecore/message-handler.c

index 431a5df..e0a921d 100644 (file)
@@ -14,10 +14,15 @@ look like that:
 {{Integer} {{1st float} {2nd float} ...}}{...}
 Any characters that are not enclosed in curly braces are ignored (all characters
 between { and {, between } and } and between } and {). The same syntax is used
-to specify message parameters. The following reference lists available messages,
+to specify message parameters. The reference further down lists available messages,
 their parameters and return values. If a return value is enclosed in {}, this
 means that multiple elements of the same type may be returned.
 
+For string parameters that contain curly braces or backslashes, those characters
+must be escaped by adding a "\" before them.
+
+Reference:
+
 Object path: /core
 Message: list-handlers
 Parameters: None
index 19f100f..df912a8 100644 (file)
@@ -711,6 +711,7 @@ libpulsecommon_@PA_MAJORMINOR@_la_SOURCES = \
                pulse/timeval.c pulse/timeval.h \
                pulse/rtclock.c pulse/rtclock.h \
                pulse/volume.c pulse/volume.h \
+               pulse/message-params.c pulse/message-params.h \
                pulsecore/atomic.h \
                pulsecore/authkey.c pulsecore/authkey.h \
                pulsecore/conf-parser.c pulsecore/conf-parser.h \
@@ -917,6 +918,7 @@ libpulse_la_SOURCES = \
                pulse/mainloop-api.c pulse/mainloop-api.h \
                pulse/mainloop-signal.c pulse/mainloop-signal.h \
                pulse/mainloop.c pulse/mainloop.h \
+               pulse/message-params.c pulse/message-params.h \
                pulse/operation.c pulse/operation.h \
                pulse/proplist.c pulse/proplist.h \
                pulse/pulseaudio.h \
@@ -929,7 +931,6 @@ libpulse_la_SOURCES = \
                pulse/timeval.c pulse/timeval.h \
                pulse/utf8.c pulse/utf8.h \
                pulse/util.c pulse/util.h \
-               pulse/message-params.c pulse/message-params.h \
                pulse/volume.c pulse/volume.h \
                pulse/xmalloc.c pulse/xmalloc.h
 
index 0acbf05..4d196c1 100644 (file)
@@ -229,8 +229,15 @@ pa_mainloop_quit;
 pa_mainloop_run;
 pa_mainloop_set_poll_func;
 pa_mainloop_wakeup;
+pa_message_params_begin_list;
+pa_message_params_end_list;
+pa_message_params_free;
+pa_message_params_new;
 pa_message_params_read_raw;
 pa_message_params_read_string;
+pa_message_params_to_string_free;
+pa_message_params_write_raw;
+pa_message_params_write_string;
 pa_msleep;
 pa_thread_make_realtime;
 pa_operation_cancel;
index 8d74a31..b84112e 100644 (file)
@@ -5,6 +5,7 @@ libpulsecommon_sources = [
   'pulse/format.c',
   'pulse/json.c',
   'pulse/mainloop-api.c',
+  'pulse/message-params.c',
   'pulse/xmalloc.c',
   'pulse/proplist.c',
   'pulse/utf8.c',
@@ -78,6 +79,7 @@ libpulsecommon_headers = [
   'pulse/format.h',
   'pulse/json.h',
   'pulse/mainloop-api.h',
+  'pulse/message-params.h',
   'pulse/xmalloc.h',
   'pulse/proplist.h',
   'pulse/utf8.h',
index 0afda4f..236800b 100644 (file)
 #include <pulse/xmalloc.h>
 
 #include <pulsecore/macro.h>
+#include <pulsecore/strbuf.h>
+#include <pulsecore/core-util.h>
 
 #include "message-params.h"
 
+/* Message parameter structure, a wrapper for pa_strbuf */
+struct pa_message_params {
+    pa_strbuf *buffer;
+};
+
 /* Split the specified string into elements. An element is defined as
  * a sub-string between curly braces. The function is needed to parse
  * the parameters of messages. Each time it is called it returns the
  * position of the current element in result and the state pointer is
- * advanced to the next list element.
+ * advanced to the next list element. On return, the parameter
+ * *is_unpacked indicates if the string is plain text or contains a
+ * sub-list. is_unpacked may be NULL.
  *
  * The variable state points to, should be initialized to NULL before
  * the first call. The function returns 1 on success, 0 if end of string
  * is encountered and -1 on parse error.
  *
  * result is set to NULL on end of string or parse error. */
-static int split_list(char *c, char **result, void **state) {
+static int split_list(char *c, char **result, bool *is_unpacked, void **state) {
     char *current = *state ? *state : c;
     uint32_t open_braces;
+    bool found_backslash = false;
 
     pa_assert(result);
 
@@ -57,29 +67,52 @@ static int split_list(char *c, char **result, void **state) {
     /* Find opening brace */
     while (*current != 0) {
 
-        if (*current == '{')
+        /* Skip escaped curly braces. */
+        if (*current == '\\' && !found_backslash) {
+            found_backslash = true;
+            current++;
+            continue;
+        }
+
+        if (*current == '{' && !found_backslash)
             break;
 
         /* unexpected closing brace, parse error */
-        if (*current == '}')
+        if (*current == '}' && !found_backslash)
             return -1;
 
+        found_backslash = false;
         current++;
     }
 
     /* No opening brace found, end of string */
     if (*current == 0)
-         return 0;
+        return 0;
 
+    if (is_unpacked)
+        *is_unpacked = true;
     *result = current + 1;
+    found_backslash = false;
     open_braces = 1;
 
     while (open_braces != 0 && *current != 0) {
         current++;
-        if (*current == '{')
+
+        /* Skip escaped curly braces. */
+        if (*current == '\\' && !found_backslash) {
+            found_backslash = true;
+            continue;
+        }
+
+        if (*current == '{' && !found_backslash) {
             open_braces++;
-        if (*current == '}')
+            if (is_unpacked)
+                *is_unpacked = false;
+        }
+        if (*current == '}' && !found_backslash)
             open_braces--;
+
+        found_backslash = false;
     }
 
     /* Parse error, closing brace missing */
@@ -96,23 +129,122 @@ static int split_list(char *c, char **result, void **state) {
     return 1;
 }
 
+/* Read functions */
+
 /* Read a string from the parameter list. The state pointer is
  * advanced to the next element of the list. Returns a pointer
- * to a sub-string within c. The result must not be freed. */
+ * to a sub-string within c. Escape characters will be removed
+ * from the string. The result must not be freed. */
 int pa_message_params_read_string(char *c, const char **result, void **state) {
     char *start_pos;
+    char *value = NULL;
     int r;
+    bool is_unpacked = true;
 
     pa_assert(result);
 
-    if ((r = split_list(c, &start_pos, state)) == 1)
-        *result = start_pos;
+    if ((r = split_list(c, &start_pos, &is_unpacked, state)) == 1)
+        value = start_pos;
+
+    /* Check if we got a plain string not containing further lists */
+    if (!is_unpacked) {
+        /* Parse error */
+        r = -1;
+        value = NULL;
+    }
+
+    if (value)
+        *result = pa_unescape(value);
 
     return r;
 }
 
-/* Another wrapper for split_list() to distinguish between reading
- * pure string data and raw data which may contain further lists. */
+/* A wrapper for split_list() to distinguish between reading pure
+ * string data and raw data which may contain further lists. */
 int pa_message_params_read_raw(char *c, char **result, void **state) {
-    return split_list(c, result, state);
+    return split_list(c, result, NULL, state);
+}
+
+/* Write functions. The functions are wrapper functions around pa_strbuf,
+ * so that the client does not need to use pa_strbuf directly. */
+
+/* Creates a new pa_message_param structure */
+pa_message_params *pa_message_params_new(void) {
+    pa_message_params *params;
+
+    params = pa_xnew(pa_message_params, 1);
+    params->buffer = pa_strbuf_new();
+
+    return params;
+}
+
+/* Frees a pa_message_params structure */
+void pa_message_params_free(pa_message_params *params) {
+    pa_assert(params);
+
+    pa_strbuf_free(params->buffer);
+    pa_xfree(params);
+}
+
+/* Converts a pa_message_param structure to string and frees the structure.
+ * The returned string needs to be freed with pa_xree(). */
+char *pa_message_params_to_string_free(pa_message_params *params) {
+    char *result;
+
+    pa_assert(params);
+
+    result = pa_strbuf_to_string_free(params->buffer);
+
+    pa_xfree(params);
+    return result;
+}
+
+/* Writes an opening curly brace */
+void pa_message_params_begin_list(pa_message_params *params) {
+
+    pa_assert(params);
+
+    pa_strbuf_putc(params->buffer, '{');
+}
+
+/* Writes a closing curly brace */
+void pa_message_params_end_list(pa_message_params *params) {
+
+    pa_assert(params);
+
+    pa_strbuf_putc(params->buffer, '}');
+}
+
+/* Writes a string to a message_params structure, adding curly braces
+ * around the string and escaping curly braces within the string. */
+void pa_message_params_write_string(pa_message_params *params, const char *value) {
+    char *output;
+
+    pa_assert(params);
+
+    /* Null value is written as empty element */
+    if (!value)
+        value = "";
+
+    output = pa_escape(value, "{}");
+    pa_strbuf_printf(params->buffer, "{%s}", output);
+
+    pa_xfree(output);
+}
+
+/* Writes a raw string to a message_params structure, adding curly braces
+ * around the string if add_braces is true. This function can be used to
+ * write parts of a string or whole parameter lists that have been prepared
+ * elsewhere (for example an array). */
+void pa_message_params_write_raw(pa_message_params *params, const char *value, bool add_braces) {
+    pa_assert(params);
+
+    /* Null value is written as empty element */
+    if (!value)
+        value = "";
+
+    if (add_braces)
+        pa_strbuf_printf(params->buffer, "{%s}", value);
+    else
+        pa_strbuf_puts(params->buffer, value);
 }
index 5c9bc1a..f30164f 100644 (file)
 
 PA_C_DECL_BEGIN
 
+/** Structure which holds a parameter list. Wrapper for pa_strbuf  \since 15.0 */
+typedef struct pa_message_params pa_message_params;
+
+/** @{ \name Read functions */
+
 /** Read raw data from a parameter list. Used to split a message parameter
- * string into list elements  \since 15.0 */
+ * string into list elements. The string returned in *result must not be freed.  \since 15.0 */
 int pa_message_params_read_raw(char *c, char **result, void **state);
 
-/** Read a string from a parameter list. \since 15.0 */
+/** Read a string from a parameter list. Escaped curly braces and backslashes
+ * will be unescaped. \since 15.0 */
 int pa_message_params_read_string(char *c, const char **result, void **state);
 
+/** @} */
+
+/** @{ \name Write functions */
+
+/** Create a new pa_message_params structure  \since 15.0 */
+pa_message_params *pa_message_params_new(void);
+
+/** Free a pa_message_params structure.  \since 15.0 */
+void pa_message_params_free(pa_message_params *params);
+
+/** Convert pa_message_params to string, free pa_message_params structure.  \since 15.0 */
+char *pa_message_params_to_string_free(pa_message_params *params);
+
+/** Start a list by writing an opening brace.  \since 15.0 */
+void pa_message_params_begin_list(pa_message_params *params);
+
+/** End a list by writing a closing brace.  \since 15.0 */
+void pa_message_params_end_list(pa_message_params *params);
+
+/** Append string to parameter list. Curly braces and backslashes will be escaped.  \since 15.0 */
+void pa_message_params_write_string(pa_message_params *params, const char *value);
+
+/** Append raw string to parameter list. Used to write incomplete strings
+ * or complete parameter lists (for example arrays). Adds curly braces around
+ * the string if add_braces is true.  \since 15.0 */
+void pa_message_params_write_raw(pa_message_params *params, const char *value, bool add_braces);
+
+/** @} */
+
 PA_C_DECL_END
 
 #endif
index da8b3b3..8b83019 100644 (file)
@@ -29,6 +29,7 @@
 #include <pulse/rtclock.h>
 #include <pulse/timeval.h>
 #include <pulse/xmalloc.h>
+#include <pulse/message-params.h>
 
 #include <pulsecore/module.h>
 #include <pulsecore/core-rtclock.h>
@@ -65,25 +66,26 @@ static void core_free(pa_object *o);
 
 /* Returns a list of handlers. */
 static char *message_handler_list(pa_core *c) {
-    pa_strbuf *buf;
+    pa_message_params *param;
     void *state = NULL;
     struct pa_message_handler *handler;
 
-    buf = pa_strbuf_new();
+    param = pa_message_params_new();
 
-    pa_strbuf_putc(buf, '{');
+    pa_message_params_begin_list(param);
     PA_HASHMAP_FOREACH(handler, c->message_handlers, state) {
-        pa_strbuf_putc(buf, '{');
+        pa_message_params_begin_list(param);
 
-        pa_strbuf_printf(buf, "{%s} {", handler->object_path);
-        if (handler->description)
-            pa_strbuf_puts(buf, handler->description);
+        /* object_path cannot contain characters that need escaping, therefore
+         * pa_message_params_write_raw() can safely be used here. */
+        pa_message_params_write_raw(param, handler->object_path, true);
+        pa_message_params_write_string(param, handler->description);
 
-        pa_strbuf_puts(buf, "}}");
+        pa_message_params_end_list(param);
     }
-    pa_strbuf_putc(buf, '}');
+    pa_message_params_end_list(param);
 
-    return pa_strbuf_to_string_free(buf);
+    return pa_message_params_to_string_free(param);
 }
 
 static int core_message_handler(const char *object_path, const char *message, char *message_parameters, char **response, void *userdata) {
index 427186d..4064455 100644 (file)
 
 #include "message-handler.h"
 
-/* Check if a string does not contain control characters. Currently these are
- * only "{" and "}". */
-static bool string_is_valid(const char *test_string) {
+/* Check if a path string starts with a / and only contains valid characters.
+ * Also reject double slashes. */
+static bool object_path_is_valid(const char *test_string) {
     uint32_t i;
 
+    if (!test_string)
+        return false;
+
+    /* Make sure the string starts with a / */
+    if (test_string[0] != '/')
+        return false;
+
     for (i = 0; test_string[i]; i++) {
-        if (test_string[i] == '{' ||
-            test_string[i] == '}')
-            return false;
+
+        if ((test_string[i] >= 'a' && test_string[i] <= 'z') ||
+            (test_string[i] >= 'A' && test_string[i] <= 'Z') ||
+            (test_string[i] >= '0' && test_string[i] <= '9') ||
+            test_string[i] == '.' ||
+            test_string[i] == '_' ||
+            test_string[i] == '-' ||
+            (test_string[i] == '/' && test_string[i + 1] != '/'))
+            continue;
+
+        return false;
     }
 
+    /* Make sure the string does not end with a / */
+    if (test_string[i - 1] == '/')
+        return false;
+
     return true;
 }
 
@@ -56,13 +75,8 @@ void pa_message_handler_register(pa_core *c, const char *object_path, const char
     pa_assert(cb);
     pa_assert(userdata);
 
-    /* Ensure that the object path is not empty and starts with "/". */
-    pa_assert(object_path[0] == '/');
-
-    /* Ensure that object path and description are valid strings */
-    pa_assert(string_is_valid(object_path));
-    if (description)
-        pa_assert(string_is_valid(description));
+    /* Ensure that object path is valid */
+    pa_assert(object_path_is_valid(object_path));
 
     handler = pa_xnew0(struct pa_message_handler, 1);
     handler->userdata = userdata;
@@ -91,7 +105,7 @@ void pa_message_handler_unregister(pa_core *c, const char *object_path) {
 int pa_message_handler_send_message(pa_core *c, const char *object_path, const char *message, const char *message_parameters, char **response) {
     struct pa_message_handler *handler;
     int ret;
-    char *parameter_copy;
+    char *parameter_copy, *path_copy;
 
     pa_assert(c);
     pa_assert(object_path);
@@ -100,8 +114,16 @@ int pa_message_handler_send_message(pa_core *c, const char *object_path, const c
 
     *response = NULL;
 
-    if (!(handler = pa_hashmap_get(c->message_handlers, object_path)))
+    path_copy = pa_xstrdup(object_path);
+
+    /* Remove trailing / from path name if present */
+    if (path_copy[strlen(path_copy) - 1] == '/')
+        path_copy[strlen(path_copy) - 1] = 0;
+
+    if (!(handler = pa_hashmap_get(c->message_handlers, path_copy))) {
+        pa_xfree(path_copy);
         return -PA_ERR_NOENTITY;
+    }
 
     parameter_copy = pa_xstrdup(message_parameters);
 
@@ -110,6 +132,7 @@ int pa_message_handler_send_message(pa_core *c, const char *object_path, const c
     ret = handler->callback(handler->object_path, message, parameter_copy, response, handler->userdata);
 
     pa_xfree(parameter_copy);
+    pa_xfree(path_copy);
     return ret;
 }
 
@@ -123,11 +146,6 @@ int pa_message_handler_set_description(pa_core *c, const char *object_path, cons
     if (!(handler = pa_hashmap_get(c->message_handlers, object_path)))
         return -PA_ERR_NOENTITY;
 
-    if (description) {
-        if (!string_is_valid(description))
-            return -PA_ERR_INVALID;
-    }
-
     pa_xfree(handler->description);
     handler->description = pa_xstrdup(description);