From: "Daniel P. Berrange" <berrange@redhat.com>
To: Markus Armbruster <armbru@redhat.com>
Cc: qemu-devel@nongnu.org, qemu-block@nongnu.org,
"Max Reitz" <mreitz@redhat.com>,
"Paolo Bonzini" <pbonzini@redhat.com>,
"Andreas Färber" <afaerber@suse.de>
Subject: Re: [Qemu-devel] [PATCH v5 01/11] qdict: implement a qdict_crumple method for un-flattening a dict
Date: Tue, 14 Jun 2016 12:39:01 +0100 [thread overview]
Message-ID: <20160614113901.GP4310@redhat.com> (raw)
In-Reply-To: <87shwma47k.fsf@dusky.pond.sub.org>
On Thu, Jun 09, 2016 at 03:20:47PM +0200, Markus Armbruster wrote:
> I apologize for the lateness of this review.
>
> "Daniel P. Berrange" <berrange@redhat.com> writes:
>
> > The qdict_flatten() method will take a dict whose elements are
> > further nested dicts/lists and flatten them by concatenating
> > keys.
> >
> > The qdict_crumple() method aims to do the reverse, taking a flat
> > qdict, and turning it into a set of nested dicts/lists. It will
> > apply nesting based on the key name, with a '.' indicating a
> > new level in the hierarchy. If the keys in the nested structure
> > are all numeric, it will create a list, otherwise it will create
> > a dict.
> >
> > If the keys are a mixture of numeric and non-numeric, or the
> > numeric keys are not in strictly ascending order, an error will
> > be reported.
> >
> > As an example, a flat dict containing
> >
> > {
> > 'foo.0.bar': 'one',
> > 'foo.0.wizz': '1',
> > 'foo.1.bar': 'two',
> > 'foo.1.wizz': '2'
> > }
> >
> > will get turned into a dict with one element 'foo' whose
> > value is a list. The list elements will each in turn be
> > dicts.
> >
> > {
> > 'foo': [
> > { 'bar': 'one', 'wizz': '1' },
> > { 'bar': 'two', 'wizz': '2' }
> > ],
> > }
> >
> > If the key is intended to contain a literal '.', then it must
> > be escaped as '..'. ie a flat dict
> >
> > {
> > 'foo..bar': 'wizz',
> > 'bar.foo..bar': 'eek',
> > 'bar.hello': 'world'
> > }
> >
> > Will end up as
> >
> > {
> > 'foo.bar': 'wizz',
> > 'bar': {
> > 'foo.bar': 'eek',
> > 'hello': 'world'
> > }
> > }
> >
> > The intent of this function is that it allows a set of QemuOpts
> > to be turned into a nested data structure that mirrors the nesting
> > used when the same object is defined over QMP.
> >
> > Signed-off-by: Daniel P. Berrange <berrange@redhat.com>
>
> > +/**
> > + * qdict_split_flat_key:
> > + * @key: the key string to split
> > + * @prefix: non-NULL pointer to hold extracted prefix
> > + * @suffix: non-NULL pointer to hold extracted suffix
> > + *
> > + * Given a flattened key such as 'foo.0.bar', split it
> > + * into two parts at the first '.' separator. Allows
> > + * double dot ('..') to escape the normal separator.
> > + *
> > + * eg
> > + * 'foo.0.bar' -> prefix='foo' and suffix='0.bar'
> > + * 'foo..0.bar' -> prefix='foo.0' and suffix='bar'
> > + *
> > + * The '..' sequence will be unescaped in the returned
> > + * 'prefix' string. The 'suffix' string will be left
> > + * in escaped format, so it can be fed back into the
> > + * qdict_split_flat_key() key as the input later.
>
> Why is the suffix strdup'ed then?
It doesn't need to be - i'll make it const.
> > +}
> > +
> > +
> > +/**
> > + * qdict_list_size:
> > + * @maybe_list: dict that may be only list elements
>
> Huh? How can a dictionary "be only list elements"?
>
> Do you mean "the dictionary to test?"
I'll say "dict to search for keys representing list elements."
> > + *
> > + * Determine whether all keys in @maybe_list are
> > + * valid list elements. If they are all valid,
> > + * then this returns the number of elements. If
> > + * they all look like non-numeric keys, then returns
> > + * zero. If there is a mix of numeric and non-numeric
> > + * keys, then an error is set as it is both a list
> > + * and a dict at once.
>
> This is well-defined only if empty @maybe_list is considered to have
> dict nature, not list nature. Else, return value zero could be the
> length of the empty list or the special "has dict nature" value.
>
> Please spell out behavior for empty @maybe_list.
Yep, empty list will imply qdict nature and so return zero.
> > + *
> > + * Returns: number of list elements, 0 if a dict, -1 on error
>
> Awkward function name. qdict_list_size_if_list() would be clear.
>
> But I'd simply turn this into a predicate qdict_is_list(), and have the
> caller use qdict_size() to get the number of elements.
I thought that qdict_size() would cause another iteration, but
I see now it just returns a cached size. So I'll switch to
qidct_is_list().
> > +static ssize_t qdict_list_size(QDict *maybe_list, Error **errp)
> > +{
> > + const QDictEntry *entry, *next;
> > + ssize_t len = 0;
> > + ssize_t max = -1;
> > + int is_list = -1;
> > + int64_t val;
> > +
> > + entry = qdict_first(maybe_list);
> > + while (entry != NULL) {
> > + next = qdict_next(maybe_list, entry);
>
> Please keep the loop control in one place:
>
> for (entry = qdict_first(maybe_list); entry; entry = qdict_next(entry)) {
>
> I'd rename some variables for less verbiage:
>
> for (ent = qdict_first(dict); ent; ent = qdict_next(ent)) {
>
> Your loop control also works when the loop body deletes @entry from
> @maybe_list. Seeing such loop control in a function that isn't supposed
> to change the its argument makes the reviewer go "WTF?!?" :)
This pattern was left from an earlier version where all the code was in
one method. I'll change it to a for() loop now.
>
> > +
> > + if (qemu_strtoll(entry->key, NULL, 10, &val) == 0) {
> > + if (is_list == -1) {
> > + is_list = 1;
> > + } else if (!is_list) {
> > + error_setg(errp,
> > + "Cannot crumple a dictionary with a mix of list "
> > + "and non-list keys");
> > + return -1;
> > + }
> > + len++;
> > + if (val > max) {
> > + max = val;
> > + }
> > + } else {
> > + if (is_list == -1) {
> > + is_list = 0;
> > + } else if (is_list) {
> > + error_setg(errp,
> > + "Cannot crumple a dictionary with a mix of list "
> > + "and non-list keys");
> > + return -1;
> > + }
> > + }
> > +
> > + entry = next;
> > + }
> > +
> > + if (is_list == -1) {
> > + is_list = 0;
>
> This can happen only when @maybe_list is empty. Okay, but perhaps you'd
> like to assert(!qdict_size(maybe_list)).
Ok
>
> > + }
> > +
> > + if (len != (max + 1)) {
> > + error_setg(errp, "List indexes are not continuous, "
> > + "saw %zd elements but %zd largest index",
> > + len, max);
> > + return -1;
>
> contiguous?
>
> What if we saw indexes 0, 2, 2?
That would imply that the dict had two entries with the same
key, which by definition is impossible with a dict data
structure.
> > + }
> > +
> > + return is_list ? len : 0;
> > +}
> > +
> > +/**
> > + * qdict_crumple:
> > + * @src: the original flat dictionary to crumple
>
> "Flat" means all values are scalar. Should we spell that out?
Yep, ok.
> > + * @recursive: true to recursively crumple nested dictionaries
> > + *
> > + * Takes a flat dictionary whose keys use '.' separator to
> > + * indicate nesting, and values are scalars, crumplings it
>
> s/, crumplings/, and crumples/
>
> > + * into a nested structure. If the @recursive parameter is
> > + * false, then only the first level of structure implied
> > + * by the keys will be crumpled. If @recursive is true,
> > + * then the input will be recursively crumpled to expand
> > + * all levels of structure in the keys.
> > + *
> > + * To include a literal '.' in a key name, it must be escaped
> > + * as '..'
> > + *
> > + * For example, an input of:
> > + *
> > + * { 'foo.0.bar': 'one', 'foo.0.wizz': '1',
> > + * 'foo.1.bar': 'two', 'foo.1.wizz': '2' }
> > + *
> > + * will result in any output of:
> > + *
> > + * {
> > + * 'foo': [
> > + * { 'bar': 'one', 'wizz': '1' },
> > + * { 'bar': 'two', 'wizz': '2' }
> > + * ],
> > + * }
> > + *
> > + * Returns: either a QDict or QList for the nested data structure
>
> I think you should discuss how this can fail.
Will do.
> > +QObject *qdict_crumple(QDict *src, bool recursive, Error **errp)
> > +{
> > + const QDictEntry *entry, *next;
> > + QDict *two_level, *multi_level = NULL;
> > + QObject *dst = NULL, *child;
> > + ssize_t list_len;
> > + size_t i;
> > + char *prefix = NULL, *suffix = NULL;
> > +
> > + two_level = qdict_new();
> > + entry = qdict_first(src);
> > +
> > + /* Step 1: split our totally flat dict into a two level dict */
> > + while (entry != NULL) {
> > + next = qdict_next(src, entry);
>
> Again, keep the loop control in one place.
>
> > +
> > + if (qobject_type(entry->value) == QTYPE_QDICT ||
> > + qobject_type(entry->value) == QTYPE_QLIST) {
> > + error_setg(errp, "Value %s is not a scalar",
> > + entry->key);
> > + goto error;
> > + }
> > +
> > + qdict_split_flat_key(entry->key, &prefix, &suffix);
> > +
> > + child = qdict_get(two_level, prefix);
> > + if (suffix) {
> > + if (child) {
> > + if (qobject_type(child) != QTYPE_QDICT) {
> > + error_setg(errp, "Key %s prefix is already set as a scalar",
> > + prefix);
> > + goto error;
> > + }
> > + } else {
> > + child = QOBJECT(qdict_new());
> > + qdict_put_obj(two_level, prefix, child);
> > + }
> > + qobject_incref(entry->value);
> > + qdict_put_obj(qobject_to_qdict(child), suffix, entry->value);
> > + } else {
> > + if (child) {
> > + error_setg(errp, "Key %s prefix is already set as a dict",
> > + prefix);
> > + goto error;
> > + }
> > + qobject_incref(entry->value);
> > + qdict_put_obj(two_level, prefix, entry->value);
> > + }
>
> Works, because we put only QDicts we've created ourselves (first
> qdict_put_obj() above) and values we got from @src (second
> qdict_put_obj()), and we fail when such a value isn't scalar.
>
> > +
> > + g_free(suffix);
>
> As I suspected, qdict_split_flat_key() strdup'ing the suffix is useless.
>
> > + g_free(prefix);
> > + suffix = prefix = NULL;
>
> Dead stores.
No, they're not dead. The end of this method has a 'g_free(prefix)' so
we must be sure to clear this out to prevent a double-free if a later
codebase jumps to the error label.
> > + entry = next;
> > + }
> > +
> > + /* Step 2: optionally process the two level dict recursively
> > + * into a multi-level dict */
> > + if (recursive) {
> > + multi_level = qdict_new();
> > + entry = qdict_first(two_level);
> > + while (entry != NULL) {
> > + next = qdict_next(two_level, entry);
>
> Again, keep the loop control in one place.
OK
>
> > +
> > + if (qobject_type(entry->value) == QTYPE_QDICT) {
> > + child = qdict_crumple(qobject_to_qdict(entry->value),
> > + recursive, errp);
> > + if (!child) {
> > + goto error;
> > + }
> > +
> > + qdict_put_obj(multi_level, entry->key, child);
> > + } else {
> > + qobject_incref(entry->value);
> > + qdict_put_obj(multi_level, entry->key, entry->value);
> > + }
> > +
> > + entry = next;
> > + }
> > + QDECREF(two_level);
> > + } else {
> > + multi_level = two_level;
> > + }
> > + two_level = NULL;
> > +
> > + /* Step 3: detect if we need to turn our dict into list */
> > + list_len = qdict_list_size(multi_level, errp);
> > + if (list_len < 0) {
> > + goto error;
> > + }
> > +
> > + if (list_len) {
> > + dst = QOBJECT(qlist_new());
> > +
> > + for (i = 0; i < list_len; i++) {
> > + char *key = g_strdup_printf("%zu", i);
> > +
> > + child = qdict_get(multi_level, key);
> > + g_free(key);
> > + assert(child);
>
> qdict_list_size() accepts as list index any (string) key qemu_strtoll()
> accepts. If %zu formats it back into the same string, we'll find it
> here. Else we die. Please spell this out in the function contract.
OK.
> [*] I'm afraid we also die if qdict_list_size()'s "List indexes are not
> continuous" check gets fooled. Suggest to drop that check, and replace
> this assertion by error_setg(errp, "Malformed list indexes").
> Admittedly not the nicest error message; perhaps you can come up with a
> better one.
We can't be fooled per my note earlier
>
> > +
> > + qobject_incref(child);
> > + qlist_append_obj(qobject_to_qlist(dst), child);
> > + }
> > + QDECREF(multi_level);
>
> Do we need
>
> multi_level = NULL;
>
> here?
It isn't needed right now, since we're at the end of the method now
and nothing will touch this var again. Setting it to NULL is a
worthwhile safety net for future refactoring though.
>
> > + } else {
> > + dst = QOBJECT(multi_level);
> > + }
> > +
> > + return dst;
> > +
> > + error:
> > + g_free(suffix);
> > + g_free(prefix);
> > + QDECREF(multi_level);
> > + QDECREF(two_level);
> > + qobject_decref(dst);
> > + return NULL;
> > +}
> > +
> > +
> > /**
> > * qdict_array_entries(): Returns the number of direct array entries if the
> > * sub-QDict of src specified by the prefix in subqdict (or src itself for
> > diff --git a/tests/check-qdict.c b/tests/check-qdict.c
> > index a43056c..0d12f40 100644
> > --- a/tests/check-qdict.c
> > +++ b/tests/check-qdict.c
> > @@ -15,6 +15,7 @@
> > #include "qapi/qmp/qint.h"
> > #include "qapi/qmp/qdict.h"
> > #include "qapi/qmp/qstring.h"
> > +#include "qapi/error.h"
> > #include "qemu-common.h"
> > +static void qdict_crumple_test_bad_inputs(void)
> > +{
> > + QDict *src;
> > + Error *error = NULL;
> > +
> > + src = qdict_new();
> > + /* rule.0 can't be both a string and a dict */
> > + qdict_put(src, "rule.0", qstring_from_str("fred"));
> > + qdict_put(src, "rule.0.policy", qstring_from_str("allow"));
> > +
> > + g_assert(qdict_crumple(src, true, &error) == NULL);
> > + g_assert(error != NULL);
> > + error_free(error);
> > + error = NULL;
> > + QDECREF(src);
> > +
> > + src = qdict_new();
> > + /* rule can't be both a list and a dict */
> > + qdict_put(src, "rule.0", qstring_from_str("fred"));
> > + qdict_put(src, "rule.a", qstring_from_str("allow"));
> > +
> > + g_assert(qdict_crumple(src, true, &error) == NULL);
> > + g_assert(error != NULL);
> > + error_free(error);
> > + error = NULL;
> > + QDECREF(src);
> > +
> > + src = qdict_new();
> > + /* The input should be flat, ie no dicts or lists */
> > + qdict_put(src, "rule.a", qdict_new());
> > + qdict_put(src, "rule.b", qstring_from_str("allow"));
> > +
> > + g_assert(qdict_crumple(src, true, &error) == NULL);
> > + g_assert(error != NULL);
> > + error_free(error);
> > + error = NULL;
> > + QDECREF(src);
> > +
> > +
> > + src = qdict_new();
> > + /* List indexes must not have gaps */
> > + qdict_put(src, "rule.0", qdict_new());
> > + qdict_put(src, "rule.3", qstring_from_str("allow"));
> > +
> > + g_assert(qdict_crumple(src, true, &error) == NULL);
> > + g_assert(error != NULL);
> > + error_free(error);
> > + error = NULL;
> > + QDECREF(src);
>
> Suggest to add test case
>
> /* List indexes must not have gaps (more creative version) */
> qdict_put(src, "rule.0", ...);
> qdict_put(src, "rule.2", ...);
> qdict_put(src, "rule.2", ...);
That's surely impossible, as dict keys have to be unique.
> and
>
> /* List indexes must be in %zu format */
> qdict_put(src, "rule.+0", ...);
Ok.
Regards,
Daniel
--
|: http://berrange.com -o- http://www.flickr.com/photos/dberrange/ :|
|: http://libvirt.org -o- http://virt-manager.org :|
|: http://autobuild.org -o- http://search.cpan.org/~danberr/ :|
|: http://entangle-photo.org -o- http://live.gnome.org/gtk-vnc :|
next prev parent reply other threads:[~2016-06-14 11:39 UTC|newest]
Thread overview: 25+ messages / expand[flat|nested] mbox.gz Atom feed top
2016-06-02 16:46 [Qemu-devel] [PATCH v5 00/11] Provide a QOM-based authorization API Daniel P. Berrange
2016-06-02 16:46 ` [Qemu-devel] [PATCH v5 01/11] qdict: implement a qdict_crumple method for un-flattening a dict Daniel P. Berrange
2016-06-09 13:20 ` Markus Armbruster
2016-06-09 13:28 ` Daniel P. Berrange
2016-06-14 11:39 ` Daniel P. Berrange [this message]
2016-06-16 9:16 ` Markus Armbruster
2016-06-02 16:46 ` [Qemu-devel] [PATCH v5 02/11] qapi: allow QmpInputVisitor to auto-cast types Daniel P. Berrange
2016-06-08 12:01 ` Paolo Bonzini
2016-06-14 14:10 ` Daniel P. Berrange
2016-06-09 14:03 ` Markus Armbruster
2016-06-14 13:25 ` Daniel P. Berrange
2016-06-16 9:23 ` Markus Armbruster
2016-06-02 16:46 ` [Qemu-devel] [PATCH v5 03/11] qom: support arbitrary non-scalar properties with -object Daniel P. Berrange
2016-06-09 14:43 ` Markus Armbruster
2016-06-14 14:16 ` Daniel P. Berrange
2016-06-02 16:46 ` [Qemu-devel] [PATCH v5 04/11] util: add QAuthZ object as an authorization base class Daniel P. Berrange
2016-06-02 16:46 ` [Qemu-devel] [PATCH v5 05/11] util: add QAuthZSimple object type for a simple access control list Daniel P. Berrange
2016-06-02 16:46 ` [Qemu-devel] [PATCH v5 06/11] acl: delete existing ACL implementation Daniel P. Berrange
2016-06-02 16:46 ` [Qemu-devel] [PATCH v5 07/11] qemu-nbd: add support for ACLs for TLS clients Daniel P. Berrange
2016-06-02 16:46 ` [Qemu-devel] [PATCH v5 08/11] nbd: allow an ACL to be set with nbd-server-start QMP command Daniel P. Berrange
2016-06-02 16:46 ` [Qemu-devel] [PATCH v5 09/11] migration: add support for a "tls-acl" migration parameter Daniel P. Berrange
2016-06-02 16:46 ` [Qemu-devel] [PATCH v5 10/11] chardev: add support for ACLs for TLS clients Daniel P. Berrange
2016-06-02 16:46 ` [Qemu-devel] [PATCH v5 11/11] vnc: allow specifying a custom ACL object name Daniel P. Berrange
2016-06-08 11:53 ` [Qemu-devel] [PATCH v5 00/11] Provide a QOM-based authorization API Daniel P. Berrange
2016-06-08 14:26 ` Markus Armbruster
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20160614113901.GP4310@redhat.com \
--to=berrange@redhat.com \
--cc=afaerber@suse.de \
--cc=armbru@redhat.com \
--cc=mreitz@redhat.com \
--cc=pbonzini@redhat.com \
--cc=qemu-block@nongnu.org \
--cc=qemu-devel@nongnu.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.