diff mbox

[v4,17/28] qapi: Factor out JSON number formatting

Message ID 1463632874-28559-18-git-send-email-eblake@redhat.com
State New
Headers show

Commit Message

Eric Blake May 19, 2016, 4:41 a.m. UTC
Pull out a new qstring_append_json_number() helper, so that all
JSON output producers can use a consistent style for printing
floating point without duplicating code (since we are doing more
data massaging than a simple printf format can handle).  (For
now, there is only one client, but later patches will use it.)

Adding a return value will let callers that care diagnose the
situation where an attempt was made to output invalid JSON (which
does not specify Infinity or NaN). None of the current callers
care, but a future patch wants to make it possible to turn this
situation into an error.

Signed-off-by: Eric Blake <eblake@redhat.com>

---
v4: keep helper in qobject-json.[ch], don't use Error **errp for
inf/NaN handling, drop R-b
v3: rebase to latest; even though the patch differs quite a bit
from v2, the changes are due to prior comment changes that are
just moving between files, so R-b kept
v2: minor wording tweaks
---
 include/qapi/qmp/qobject-json.h |  1 +
 qobject/qobject-json.c          | 63 +++++++++++++++++++++++++----------------
 2 files changed, 40 insertions(+), 24 deletions(-)

Comments

Markus Armbruster June 2, 2016, 3:02 p.m. UTC | #1
Eric Blake <eblake@redhat.com> writes:

> Pull out a new qstring_append_json_number() helper, so that all
> JSON output producers can use a consistent style for printing
> floating point without duplicating code (since we are doing more
> data massaging than a simple printf format can handle).  (For
> now, there is only one client, but later patches will use it.)
>
> Adding a return value will let callers that care diagnose the
> situation where an attempt was made to output invalid JSON (which
> does not specify Infinity or NaN). None of the current callers
> care, but a future patch wants to make it possible to turn this
> situation into an error.
>
> Signed-off-by: Eric Blake <eblake@redhat.com>
[...]
> @@ -303,3 +282,39 @@ int qstring_append_json_string(QString *qstring, const char *str)
>      qstring_append(qstring, "\"");
>      return result;
>  }
> +
> +/**
> + * Append a JSON representation of @number to @qstring.
> + *
> + * Returns -1 if the added text is not strict JSON, or 0 if the number
> + * was finite.
> + */

Suggest:

 * Return 0 if the number is finite, as required by RFC 7159, else -1.

The return value makes some sense only for symmetry with
qstring_append_json_string().  Without that, I'd ask you to keep this
function simple.  Callers could just as easily test isfinite()
themselves.

> +int qstring_append_json_number(QString *qstring, double number)
> +{
> +    char buffer[1024];
> +    int len;
> +
> +    /* FIXME: snprintf() is locale dependent; but JSON requires
> +     * numbers to be formatted as if in the C locale. Dependence
> +     * on C locale is a pervasive issue in QEMU. */
> +    /* FIXME: This risks printing Inf or NaN, which are not valid
> +     * JSON values. */
> +    /* FIXME: the default precision of 6 for %f often causes
> +     * rounding errors; we should be using DBL_DECIMAL_DIG (17),
> +     * and only rounding to a shorter number if the result would
> +     * still produce the same floating point value.  */
> +    len = snprintf(buffer, sizeof(buffer), "%f", number);
> +    assert(len > 0 && len < sizeof(buffer));
> +    while (len > 0 && buffer[len - 1] == '0') {
> +        len--;
> +    }
> +
> +    if (len && buffer[len - 1] == '.') {
> +        buffer[len - 1] = 0;
> +    } else {
> +        buffer[len] = 0;
> +    }
> +
> +    qstring_append(qstring, buffer);
> +    return isfinite(number) ? 0 : -1;
> +}
Eric Blake June 2, 2016, 3:06 p.m. UTC | #2
On 06/02/2016 09:02 AM, Markus Armbruster wrote:
> Eric Blake <eblake@redhat.com> writes:
> 
>> Pull out a new qstring_append_json_number() helper, so that all
>> JSON output producers can use a consistent style for printing
>> floating point without duplicating code (since we are doing more
>> data massaging than a simple printf format can handle).  (For
>> now, there is only one client, but later patches will use it.)
>>
>> Adding a return value will let callers that care diagnose the
>> situation where an attempt was made to output invalid JSON (which
>> does not specify Infinity or NaN). None of the current callers
>> care, but a future patch wants to make it possible to turn this
>> situation into an error.

> 
> Suggest:
> 
>  * Return 0 if the number is finite, as required by RFC 7159, else -1.
> 
> The return value makes some sense only for symmetry with
> qstring_append_json_string().  Without that, I'd ask you to keep this
> function simple.  Callers could just as easily test isfinite()
> themselves.

I'm actually thinking of modifying this, given the recent thread
pointing out that libvirt chokes hard on JSON extensions:

https://lists.gnu.org/archive/html/qemu-devel/2016-05/msg04398.html

That is, for symmetry with qstring_append_json_string(), I'm thinking of
changing NaN to 0 and Inf to DBL_MAX, and always outputting a finite
value, in addition to returning -1 to inform the caller that a
substitution was made, so that the output is always strict JSON.
Markus Armbruster June 3, 2016, 9:02 a.m. UTC | #3
Eric Blake <eblake@redhat.com> writes:

> On 06/02/2016 09:02 AM, Markus Armbruster wrote:
>> Eric Blake <eblake@redhat.com> writes:
>> 
>>> Pull out a new qstring_append_json_number() helper, so that all
>>> JSON output producers can use a consistent style for printing
>>> floating point without duplicating code (since we are doing more
>>> data massaging than a simple printf format can handle).  (For
>>> now, there is only one client, but later patches will use it.)
>>>
>>> Adding a return value will let callers that care diagnose the
>>> situation where an attempt was made to output invalid JSON (which
>>> does not specify Infinity or NaN). None of the current callers
>>> care, but a future patch wants to make it possible to turn this
>>> situation into an error.
>
>> 
>> Suggest:
>> 
>>  * Return 0 if the number is finite, as required by RFC 7159, else -1.
>> 
>> The return value makes some sense only for symmetry with
>> qstring_append_json_string().  Without that, I'd ask you to keep this
>> function simple.  Callers could just as easily test isfinite()
>> themselves.
>
> I'm actually thinking of modifying this, given the recent thread
> pointing out that libvirt chokes hard on JSON extensions:
>
> https://lists.gnu.org/archive/html/qemu-devel/2016-05/msg04398.html
>
> That is, for symmetry with qstring_append_json_string(), I'm thinking of
> changing NaN to 0 and Inf to DBL_MAX, and always outputting a finite
> value, in addition to returning -1 to inform the caller that a
> substitution was made, so that the output is always strict JSON.

Mapping infinities to DBL_MIN and DBL_MAX is debatable, but mapping NaN
to zero is outright wrong.

If we decide QMP should stick to JSON here and avoid non-finite numbers,
we need to treat an attempt to emit a non-finite number as a bug:
assert(isfinite(...)).  Making sure nothing ever attempts to emit such
numbers will be tedious.

If we decide QMP should remain as it is, we need to document non-finite
numbers among its JSON extensions.  We should also fix our QMP parsers
to accept non-finite numbers then.  Including the one in libvirt.
Attempts to emit non-finite numbers then *may* be bugs.  Really no
different than finite numbers outside their intended range, such as a
negative size.  Catching these bugs is of course also tedious.  The
difference is that they manifest in QMP as semantic instead of lexical
errors.  Lexical errors are the worst to handle gracefully.
Eric Blake June 9, 2016, 4:07 p.m. UTC | #4
On 06/03/2016 03:02 AM, Markus Armbruster wrote:

>>> Suggest:
>>>
>>>  * Return 0 if the number is finite, as required by RFC 7159, else -1.
>>>
>>> The return value makes some sense only for symmetry with
>>> qstring_append_json_string().  Without that, I'd ask you to keep this
>>> function simple.  Callers could just as easily test isfinite()
>>> themselves.
>>
>> I'm actually thinking of modifying this, given the recent thread
>> pointing out that libvirt chokes hard on JSON extensions:
>>
>> https://lists.gnu.org/archive/html/qemu-devel/2016-05/msg04398.html
>>
>> That is, for symmetry with qstring_append_json_string(), I'm thinking of
>> changing NaN to 0 and Inf to DBL_MAX, and always outputting a finite
>> value, in addition to returning -1 to inform the caller that a
>> substitution was made, so that the output is always strict JSON.
> 
> Mapping infinities to DBL_MIN and DBL_MAX is debatable, but mapping NaN
> to zero is outright wrong.

How about this alternative:

Finite values remain numbers:
"number":1

But non-finite values are output as strings, so that our output is
always valid JSON - the recipient may not be expecting a string in place
of a number, but at least should be able to parse the output rather than
choking hard.
"number":"nan"

The return value -1 then indicates that a stringized replacement was
used, so that any later patch can use a strict flag on whether to allow
the replacement output or assert.

> 
> If we decide QMP should stick to JSON here and avoid non-finite numbers,
> we need to treat an attempt to emit a non-finite number as a bug:
> assert(isfinite(...)).  Making sure nothing ever attempts to emit such
> numbers will be tedious.
> 
> If we decide QMP should remain as it is, we need to document non-finite
> numbers among its JSON extensions.  We should also fix our QMP parsers
> to accept non-finite numbers then.  Including the one in libvirt.
> Attempts to emit non-finite numbers then *may* be bugs.  Really no
> different than finite numbers outside their intended range, such as a
> negative size.  Catching these bugs is of course also tedious.  The
> difference is that they manifest in QMP as semantic instead of lexical
> errors.  Lexical errors are the worst to handle gracefully.

I may still try to tackle fixing the QMP parser to accept NaN and
infinity on input (since it's hand-written, we at least have control
over that) - it will certainly be easier than getting libvirt to parse
non-finite numbers (libvirt uses libyajl, and my emails to the yajl
mailing list have gone unanswered, making me think the project is not
very vibrant and thus not very patchable).  But with my proposal of
producing a stringized non-finite value, we at least convert lexical
into semantic errors, which I agree with your assessment is a nicer way
of dealing with it.

Of course, a policy change of outputting stringized non-finite numbers
should be separate from refactoring patches that just move functions around.
Markus Armbruster June 13, 2016, 8:22 a.m. UTC | #5
Eric Blake <eblake@redhat.com> writes:

> On 06/03/2016 03:02 AM, Markus Armbruster wrote:
>
>>>> Suggest:
>>>>
>>>>  * Return 0 if the number is finite, as required by RFC 7159, else -1.
>>>>
>>>> The return value makes some sense only for symmetry with
>>>> qstring_append_json_string().  Without that, I'd ask you to keep this
>>>> function simple.  Callers could just as easily test isfinite()
>>>> themselves.
>>>
>>> I'm actually thinking of modifying this, given the recent thread
>>> pointing out that libvirt chokes hard on JSON extensions:
>>>
>>> https://lists.gnu.org/archive/html/qemu-devel/2016-05/msg04398.html
>>>
>>> That is, for symmetry with qstring_append_json_string(), I'm thinking of
>>> changing NaN to 0 and Inf to DBL_MAX, and always outputting a finite
>>> value, in addition to returning -1 to inform the caller that a
>>> substitution was made, so that the output is always strict JSON.
>> 
>> Mapping infinities to DBL_MIN and DBL_MAX is debatable, but mapping NaN
>> to zero is outright wrong.
>
> How about this alternative:
>
> Finite values remain numbers:
> "number":1
>
> But non-finite values are output as strings, so that our output is
> always valid JSON - the recipient may not be expecting a string in place
> of a number, but at least should be able to parse the output rather than
> choking hard.
> "number":"nan"
>
> The return value -1 then indicates that a stringized replacement was
> used, so that any later patch can use a strict flag on whether to allow
> the replacement output or assert.

I dislike this a lot less than mapping non-finite numbers to finite
ones.

I still dislike it, because it defeats fitting a schema to QMP: instead
of the true JSON type "number", we'd need the sum type of "number" and
"string", which this really isn't: only a few special strings are valid,
and they're not actually strings.  If the schema language can do sum
types, we'd even be stuck with their common super-type.

The most practical solution isn't always a likable one, though.

>> If we decide QMP should stick to JSON here and avoid non-finite numbers,
>> we need to treat an attempt to emit a non-finite number as a bug:
>> assert(isfinite(...)).  Making sure nothing ever attempts to emit such
>> numbers will be tedious.
>> 
>> If we decide QMP should remain as it is, we need to document non-finite
>> numbers among its JSON extensions.  We should also fix our QMP parsers
>> to accept non-finite numbers then.  Including the one in libvirt.
>> Attempts to emit non-finite numbers then *may* be bugs.  Really no
>> different than finite numbers outside their intended range, such as a
>> negative size.  Catching these bugs is of course also tedious.  The
>> difference is that they manifest in QMP as semantic instead of lexical
>> errors.  Lexical errors are the worst to handle gracefully.
>
> I may still try to tackle fixing the QMP parser to accept NaN and
> infinity on input (since it's hand-written, we at least have control
> over that)

Making json-lexer.c recognize infinities and NaNs in strtod() syntax
shouldn't be hard.  I'd omit nan(n-char-sequence-opt), because its
semantics are implementation defined.  I'd also omit all spellings other
than inf and nan.  That leaves inf, +inf, -inf, nan, +nan, -nan.

>            - it will certainly be easier than getting libvirt to parse
> non-finite numbers (libvirt uses libyajl, and my emails to the yajl
> mailing list have gone unanswered, making me think the project is not
> very vibrant and thus not very patchable).

Nobody likes to carry downstream patches, but an unresponsive upstream
may leave you no choice.

>                                             But with my proposal of
> producing a stringized non-finite value, we at least convert lexical
> into semantic errors, which I agree with your assessment is a nicer way
> of dealing with it.
>
> Of course, a policy change of outputting stringized non-finite numbers
> should be separate from refactoring patches that just move functions around.

Yes.
Eric Blake June 13, 2016, 12:34 p.m. UTC | #6
On 06/13/2016 02:22 AM, Markus Armbruster wrote:

>> I may still try to tackle fixing the QMP parser to accept NaN and
>> infinity on input (since it's hand-written, we at least have control
>> over that)
> 
> Making json-lexer.c recognize infinities and NaNs in strtod() syntax
> shouldn't be hard.  I'd omit nan(n-char-sequence-opt), because its
> semantics are implementation defined.  I'd also omit all spellings other
> than inf and nan.  That leaves inf, +inf, -inf, nan, +nan, -nan.

+inf and +nan weren't worth it (JSON doesn't accept +0 either), but
'-inf' and '-nan' were easy.  In fact, '-infinity' was easy too - see
the posted patches:
https://lists.gnu.org/archive/html/qemu-devel/2016-06/msg02652.html
and you are right that it was not worth 'nan(n-char-seq)'.

> 
>>            - it will certainly be easier than getting libvirt to parse
>> non-finite numbers (libvirt uses libyajl, and my emails to the yajl
>> mailing list have gone unanswered, making me think the project is not
>> very vibrant and thus not very patchable).
> 
> Nobody likes to carry downstream patches, but an unresponsive upstream
> may leave you no choice.

Another (possibly-ugly) option that I thought of over the weekend: We
have 'qmp_capabilities' for handshakes between client and server.  The
server could advertise a new capability 'nonfinite' in its initial
greeting, and if the client replies with the same capability, then the
server knows that the client is prepared to accept bareword non-finite
numbers.  A client that doesn't see the server advertise anything has no
guarantees, and a server that knows the capability but doesn't see the
client request the capability should avoid sending bareword non-finite.
Markus Armbruster June 13, 2016, 2:41 p.m. UTC | #7
Eric Blake <eblake@redhat.com> writes:

> On 06/13/2016 02:22 AM, Markus Armbruster wrote:
>
>>> I may still try to tackle fixing the QMP parser to accept NaN and
>>> infinity on input (since it's hand-written, we at least have control
>>> over that)
>> 
>> Making json-lexer.c recognize infinities and NaNs in strtod() syntax
>> shouldn't be hard.  I'd omit nan(n-char-sequence-opt), because its
>> semantics are implementation defined.  I'd also omit all spellings other
>> than inf and nan.  That leaves inf, +inf, -inf, nan, +nan, -nan.
>
> +inf and +nan weren't worth it (JSON doesn't accept +0 either), but
> '-inf' and '-nan' were easy.  In fact, '-infinity' was easy too - see
> the posted patches:
> https://lists.gnu.org/archive/html/qemu-devel/2016-06/msg02652.html
> and you are right that it was not worth 'nan(n-char-seq)'.
>
>> 
>>>            - it will certainly be easier than getting libvirt to parse
>>> non-finite numbers (libvirt uses libyajl, and my emails to the yajl
>>> mailing list have gone unanswered, making me think the project is not
>>> very vibrant and thus not very patchable).
>> 
>> Nobody likes to carry downstream patches, but an unresponsive upstream
>> may leave you no choice.
>
> Another (possibly-ugly) option that I thought of over the weekend: We
> have 'qmp_capabilities' for handshakes between client and server.  The
> server could advertise a new capability 'nonfinite' in its initial
> greeting, and if the client replies with the same capability, then the
> server knows that the client is prepared to accept bareword non-finite
> numbers.  A client that doesn't see the server advertise anything has no
> guarantees, and a server that knows the capability but doesn't see the
> client request the capability should avoid sending bareword non-finite.

The capability negotiation mechanism exists to support things like that.
However, the problem what to do when the command reply contains
non-finite numbers remains.  A capability can at best hide whatever ugly
solution we have for that, and perhaps help us get rid of it in an
orderly fashion eventually, by refusing to talk to clients that don't
accept the capability.

Moreover, we're already sending non-finite numbers!  Changing QMP to
send something else unless the client opts into non-finite numbers via
capability "nonfinite" is an incompatible change.  We could weasel out
of that by declaring our sending of them a bug, then turn around and
declare it a feature when capability "nonfinite" is accepted.  Meh.
diff mbox

Patch

diff --git a/include/qapi/qmp/qobject-json.h b/include/qapi/qmp/qobject-json.h
index aa8ddd7..0749e7e 100644
--- a/include/qapi/qmp/qobject-json.h
+++ b/include/qapi/qmp/qobject-json.h
@@ -25,5 +25,6 @@  QString *qobject_to_json(const QObject *obj);
 QString *qobject_to_json_pretty(const QObject *obj);

 int qstring_append_json_string(QString *qstring, const char *str);
+int qstring_append_json_number(QString *qstring, double number);

 #endif /* QJSON_H */
diff --git a/qobject/qobject-json.c b/qobject/qobject-json.c
index bc3634f..08daeed 100644
--- a/qobject/qobject-json.c
+++ b/qobject/qobject-json.c
@@ -18,6 +18,7 @@ 
 #include "qapi/qmp/qobject-json.h"
 #include "qemu/unicode.h"
 #include "qapi/qmp/types.h"
+#include <math.h>

 typedef struct JSONParsingState
 {
@@ -180,30 +181,8 @@  static void to_json(const QObject *obj, QString *str, int pretty, int indent)
     }
     case QTYPE_QFLOAT: {
         QFloat *val = qobject_to_qfloat(obj);
-        char buffer[1024];
-        int len;
-
-        /* FIXME: snprintf() is locale dependent; but JSON requires
-         * numbers to be formatted as if in the C locale. Dependence
-         * on C locale is a pervasive issue in QEMU. */
-        /* FIXME: This risks printing Inf or NaN, which are not valid
-         * JSON values. */
-        /* FIXME: the default precision of 6 for %f often causes
-         * rounding errors; we should be using DBL_DECIMAL_DIG (17),
-         * and only rounding to a shorter number if the result would
-         * still produce the same floating point value.  */
-        len = snprintf(buffer, sizeof(buffer), "%f", qfloat_get_double(val));
-        while (len > 0 && buffer[len - 1] == '0') {
-            len--;
-        }
-
-        if (len && buffer[len - 1] == '.') {
-            buffer[len - 1] = 0;
-        } else {
-            buffer[len] = 0;
-        }
-
-        qstring_append(str, buffer);
+        /* FIXME: no way inform user if we generated invalid JSON */
+        qstring_append_json_number(str, qfloat_get_double(val));
         break;
     }
     case QTYPE_QBOOL: {
@@ -303,3 +282,39 @@  int qstring_append_json_string(QString *qstring, const char *str)
     qstring_append(qstring, "\"");
     return result;
 }
+
+/**
+ * Append a JSON representation of @number to @qstring.
+ *
+ * Returns -1 if the added text is not strict JSON, or 0 if the number
+ * was finite.
+ */
+int qstring_append_json_number(QString *qstring, double number)
+{
+    char buffer[1024];
+    int len;
+
+    /* FIXME: snprintf() is locale dependent; but JSON requires
+     * numbers to be formatted as if in the C locale. Dependence
+     * on C locale is a pervasive issue in QEMU. */
+    /* FIXME: This risks printing Inf or NaN, which are not valid
+     * JSON values. */
+    /* FIXME: the default precision of 6 for %f often causes
+     * rounding errors; we should be using DBL_DECIMAL_DIG (17),
+     * and only rounding to a shorter number if the result would
+     * still produce the same floating point value.  */
+    len = snprintf(buffer, sizeof(buffer), "%f", number);
+    assert(len > 0 && len < sizeof(buffer));
+    while (len > 0 && buffer[len - 1] == '0') {
+        len--;
+    }
+
+    if (len && buffer[len - 1] == '.') {
+        buffer[len - 1] = 0;
+    } else {
+        buffer[len] = 0;
+    }
+
+    qstring_append(qstring, buffer);
+    return isfinite(number) ? 0 : -1;
+}