Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions include/fluent-bit/flb_pack.h
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,15 @@ flb_sds_t flb_pack_msgpack_to_json_format(const char *data, uint64_t bytes,
int flb_pack_to_json_format_type(const char *str);
int flb_pack_to_json_date_type(const char *str);

/*
* Convert a double to its shortest string representation that round-trips
* back to the same value (e.g. 0.072 -> "0.072" instead of
* "0.07199999999999999"). Non-finite values (nan/inf) fall back to the
* legacy "%.17g" formatting. A 64-byte buffer is always sufficient. Returns
* the number of bytes written (excluding the NUL) or -1 on error.
*/
int flb_pack_double_to_str(double val, char *out_buf, size_t buf_size);

void flb_pack_print(const char *data, size_t bytes);
int flb_msgpack_to_json(char *json_str, size_t str_len,
const msgpack_object *obj,
Expand Down
69 changes: 63 additions & 6 deletions src/flb_pack.c
Original file line number Diff line number Diff line change
Expand Up @@ -981,6 +981,63 @@ static inline int key_exists_in_map(msgpack_object key, msgpack_object map, int
return FLB_FALSE;
}

int flb_pack_double_to_str(double val, char *out_buf, size_t buf_size)
{
int len;

if (out_buf == NULL || buf_size == 0) {
return -1;
}

#ifdef FLB_HAVE_YYJSON
if (isfinite(val)) {
/* yyjson_write_number() requires at least 40 bytes for a double */
char tmp[64];
char *end;
/*
* Build a transient yyjson real value on the stack and let yyjson's
* dtoa produce the shortest representation that round-trips back to
* the same double (e.g. 0.072 -> "0.072"). The value is zero
* initialized because yyjson_set_real() inspects the existing tag
* before overwriting it.
*/
yyjson_val num = { 0 };

yyjson_set_real(&num, val);
Comment thread
vitaly-sinev marked this conversation as resolved.
end = yyjson_write_number(&num, tmp);
if (end != NULL) {
len = (int) (end - tmp);
if (len >= 0 && (size_t) len < buf_size) {
memcpy(out_buf, tmp, len);
out_buf[len] = '\0';
return len;
}
}
}
#endif

/*
* Non-finite (nan/inf), unexpected yyjson failure, or a build without the
* yyjson backend. Integer-valued doubles keep a trailing ".0" (matching
* the yyjson output and the pre-existing behavior); other values use a
* round-trip-safe precision. floor()/fabs() avoid the undefined behavior
* of casting an out-of-range double to a long long.
*/
if (isfinite(val) && val == floor(val) && fabs(val) < 1e15) {
len = snprintf(out_buf, buf_size, "%.1f", val);
}
else {
len = snprintf(out_buf, buf_size, "%.17g", val);
}
if (len < 0) {
return -1;
}
if ((size_t) len >= buf_size) {
len = (int) (buf_size - 1);
}
return len;
}

static int msgpack2json(char *buf, int *off, size_t left,
const msgpack_object *o, int escape_unicode)
{
Expand Down Expand Up @@ -1019,15 +1076,15 @@ static int msgpack2json(char *buf, int *off, size_t left,
case MSGPACK_OBJECT_FLOAT32:
case MSGPACK_OBJECT_FLOAT64:
{
char temp[512] = {0};
if (o->via.f64 == (double)(long long int)o->via.f64) {
i = snprintf(temp, sizeof(temp)-1, "%.1f", o->via.f64);
}
else if (convert_nan_to_null && isnan(o->via.f64) ) {
char temp[64] = {0};
if (convert_nan_to_null && isnan(o->via.f64) ) {
i = snprintf(temp, sizeof(temp)-1, "null");
}
else {
i = snprintf(temp, sizeof(temp)-1, "%.16g", o->via.f64);
i = flb_pack_double_to_str(o->via.f64, temp, sizeof(temp));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve bounded JSON sizes for large floats

When flb_msgpack_to_json() is used by callers with fixed buffers, this new formatter can expand each 9-byte MessagePack double such as 1e20 into 100000000000000000000.0 (23 bytes) instead of the old compact exponent form. I checked existing direct callers: the S3 and Chronicle log_key extraction paths allocate only about the input MessagePack size (bytes + bytes / 4) and only treat ret < 0 as failure, so an array/object log key containing several such floats can make flb_msgpack_to_json() return 0 for insufficient space and then be emitted as an empty/truncated value. Please either keep exponent notation for these magnitudes or update the fixed-buffer callers to grow/retry and handle ret == 0 before switching this core path.

Useful? React with 👍 / 👎.

if (i < 0) {
goto msg2json_end;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
ret = try_to_write(buf, off, left, temp, i);
}
Expand Down
127 changes: 127 additions & 0 deletions tests/internal/pack.c
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#include <fcntl.h>
#include <unistd.h>
#include <math.h> /* for NAN */
#include <stdlib.h> /* for strtod */


#include "flb_tests_internal.h"
Expand Down Expand Up @@ -928,6 +929,130 @@ void test_json_pack_nan()
flb_pack_init(&config);
}

/*
* https://github.com/fluent/fluent-bit/issues/6447
* Floating point values must be serialized using their shortest
* representation that round-trips back to the same double, so that e.g.
* 0.072 is emitted as "0.072" and not "0.07199999999999999".
*/
void test_json_pack_float()
{
int i;
int ret;
char json_str[256];
msgpack_sbuffer mp_sbuf;
msgpack_packer mp_pck;
msgpack_object obj;
msgpack_zone mempool;
struct float_case {
double val;
const char *expected;
};
struct float_case cases[] = {
{ 0.072, "0.072" },
{ 0.1, "0.1" },
{ 3.14, "3.14" },
{ 1.005, "1.005" },
{ 5.0, "5.0" },
{ -0.0, "-0.0" },
};

for (i = 0; i < (int) (sizeof(cases) / sizeof(cases[0])); i++) {
memset(json_str, 0, sizeof(json_str));

msgpack_sbuffer_init(&mp_sbuf);
msgpack_packer_init(&mp_pck, &mp_sbuf, msgpack_sbuffer_write);
msgpack_pack_double(&mp_pck, cases[i].val);
msgpack_zone_init(&mempool, 2048);
msgpack_unpack(mp_sbuf.data, mp_sbuf.size, NULL, &mempool, &obj);

ret = flb_msgpack_to_json(&json_str[0], sizeof(json_str), &obj,
FLB_TRUE);
TEST_CHECK(ret >= 0);

/* the serialized token must always parse back to the same value */
if (!TEST_CHECK(strtod(json_str, NULL) == cases[i].val)) {
TEST_MSG("value %.17g did not round-trip, got \"%s\"",
cases[i].val, json_str);
}
#ifdef FLB_HAVE_YYJSON
/* with the yyjson backend the shortest number token is emitted */
if (!TEST_CHECK(strcmp(json_str, cases[i].expected) == 0)) {
TEST_MSG("expected \"%s\", got \"%s\"",
cases[i].expected, json_str);
}
#endif

msgpack_zone_destroy(&mempool);
msgpack_sbuffer_destroy(&mp_sbuf);
}
}

/*
* Direct coverage of flb_pack_double_to_str(), including the non-finite
* fallback path, negatives, exponent and subnormal formatting, and the
* undersized-buffer boundary.
*/
void test_pack_double_to_str()
{
int i;
int len;
char buf[64];
double vals[] = {
0.072, -0.072, 0.1, 3.14, 1.005, 5.0, 100.0, 0.0, -0.0,
1e-7, 1e20, 1e-300, 1e300,
2.2250738585072014e-308, /* DBL_MIN */
4.9e-324 /* smallest subnormal */
};

/* every finite value must round-trip back to the same double */
for (i = 0; i < (int) (sizeof(vals) / sizeof(vals[0])); i++) {
len = flb_pack_double_to_str(vals[i], buf, sizeof(buf));
TEST_CHECK(len > 0);
TEST_CHECK((int) strlen(buf) == len);
if (!TEST_CHECK(strtod(buf, NULL) == vals[i])) {
TEST_MSG("value %.17g rendered as \"%s\" did not round-trip",
vals[i], buf);
}
}

#ifdef FLB_HAVE_YYJSON
/* known shortest representations (only with the yyjson backend) */
flb_pack_double_to_str(0.072, buf, sizeof(buf));
TEST_CHECK(strcmp(buf, "0.072") == 0);
flb_pack_double_to_str(5.0, buf, sizeof(buf));
TEST_CHECK(strcmp(buf, "5.0") == 0);
flb_pack_double_to_str(-0.0, buf, sizeof(buf));
TEST_CHECK(strcmp(buf, "-0.0") == 0);
#else
/* without yyjson, integer-valued doubles still keep the float marker */
flb_pack_double_to_str(5.0, buf, sizeof(buf));
TEST_CHECK(strcmp(buf, "5.0") == 0);
flb_pack_double_to_str(-0.0, buf, sizeof(buf));
TEST_CHECK(strcmp(buf, "-0.0") == 0);
#endif

/* non-finite values fall back to legacy formatting */
flb_pack_double_to_str(NAN, buf, sizeof(buf));
TEST_CHECK(strcmp(buf, "nan") == 0);
flb_pack_double_to_str(INFINITY, buf, sizeof(buf));
TEST_CHECK(strcmp(buf, "inf") == 0);
flb_pack_double_to_str(-INFINITY, buf, sizeof(buf));
TEST_CHECK(strcmp(buf, "-inf") == 0);

/* an undersized buffer must truncate safely, never overflow */
{
char small[4];
len = flb_pack_double_to_str(3.14159, small, sizeof(small));
TEST_CHECK(len >= 0 && len < (int) sizeof(small));
TEST_CHECK((int) strlen(small) == len);
}

/* invalid arguments must be rejected without touching memory */
TEST_CHECK(flb_pack_double_to_str(3.14, NULL, sizeof(buf)) == -1);
TEST_CHECK(flb_pack_double_to_str(3.14, buf, 0) == -1);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

static int check_msgpack_val(msgpack_object obj, int expected_type, char *expected_val)
{
int len;
Expand Down Expand Up @@ -1303,6 +1428,8 @@ TEST_LIST = {
{ "json_pack_bug342" , test_json_pack_bug342},
{ "json_pack_bug1278" , test_json_pack_bug1278},
{ "json_pack_nan" , test_json_pack_nan},
{ "json_pack_float" , test_json_pack_float},
{ "pack_double_to_str" , test_pack_double_to_str},
{ "json_pack_bug5336" , test_json_pack_bug5336},
{ "json_pack_empty_array", test_json_pack_empty_array},
{ "json_date_iso8601" , test_json_date_iso8601},
Expand Down