common: avoid locale dependent strtod(3)

Replace `json_to_double()` (which uses `strtod(3)`) with our own
floating-point parsing function `json_to_millionths()` that
specifically expects to receive such a number that can fit in a
64 bit integer after being multiplied by 1 million.

The main piece of the code in this patch comes from
https://github.com/ElementsProject/lightning/pull/3535#discussion_r381041419

Changelog-None
This commit is contained in:
Rusty Russell 2020-02-19 11:11:36 +01:00
parent 89ceb273f5
commit 73ad9b5c0a
4 changed files with 104 additions and 11 deletions

View File

@ -13,8 +13,10 @@
#include <common/json.h>
#include <common/json_stream.h>
#include <common/node_id.h>
#include <common/overflows.h>
#include <common/utils.h>
#include <common/wireaddr.h>
#include <ctype.h>
#include <errno.h>
#include <inttypes.h>
#include <stdarg.h>
@ -97,13 +99,45 @@ bool json_to_s64(const char *buffer, const jsmntok_t *tok, s64 *num)
return true;
}
bool json_to_double(const char *buffer, const jsmntok_t *tok, double *num)
bool json_to_millionths(const char *buffer, const jsmntok_t *tok,
u64 *millionths)
{
char *end;
int decimal_places = -1;
bool has_digits = 0;
*num = strtod(buffer + tok->start, &end);
if (end != buffer + tok->end)
*millionths = 0;
for (int i = tok->start; i < tok->end; i++) {
if (isdigit(buffer[i])) {
has_digits = true;
/* Ignore too much precision */
if (decimal_places >= 0 && ++decimal_places > 6)
continue;
if (mul_overflows_u64(*millionths, 10))
return false;
*millionths *= 10;
if (add_overflows_u64(*millionths, buffer[i] - '0'))
return false;
*millionths += buffer[i] - '0';
} else if (buffer[i] == '.') {
if (decimal_places != -1)
return false;
decimal_places = 0;
} else
return false;
}
if (!has_digits)
return false;
if (decimal_places == -1)
decimal_places = 0;
while (decimal_places < 6) {
if (mul_overflows_u64(*millionths, 10))
return false;
*millionths *= 10;
decimal_places++;
}
return true;
}

View File

@ -69,8 +69,13 @@ bool json_to_u32(const char *buffer, const jsmntok_t *tok,
bool json_to_u16(const char *buffer, const jsmntok_t *tok,
uint16_t *num);
/* Extract double from this (must be a number literal) */
bool json_to_double(const char *buffer, const jsmntok_t *tok, double *num);
/*
* Extract a non-negative (either 0 or positive) floating-point number from this
* (must be a number literal), multiply it by 1 million and return it as an
* integer. Any fraction smaller than 0.000001 is ignored.
*/
bool json_to_millionths(const char *buffer, const jsmntok_t *tok,
u64 *millionths);
/* Extract signed integer from this (may be a string, or a number literal) */
bool json_to_int(const char *buffer, const jsmntok_t *tok, int *num);

View File

@ -39,12 +39,9 @@ struct command_result *param_millionths(struct command *cmd, const char *name,
const char *buffer,
const jsmntok_t *tok, uint64_t **num)
{
double d;
if (json_to_double(buffer, tok, &d) && d >= 0.0) {
*num = tal(cmd, uint64_t);
**num = (uint64_t)(d * 1000000);
*num = tal(cmd, uint64_t);
if (json_to_millionths(buffer, tok, *num))
return NULL;
}
return command_fail(
cmd, JSONRPC2_INVALID_PARAMS,

View File

@ -46,6 +46,62 @@ static int test_json_tok_bitcoin_amount(void)
return 0;
}
static void do_json_tok_millionths(const char *val, bool ok, uint64_t expected)
{
uint64_t amount;
jsmntok_t tok;
tok.start = 0;
tok.end = strlen(val);
assert(json_to_millionths(val, &tok, &amount) == ok);
if (ok)
assert(amount == expected);
}
static int test_json_tok_millionths(void)
{
do_json_tok_millionths("", false, 0);
do_json_tok_millionths("0..0", false, 0);
do_json_tok_millionths("0.0.", false, 0);
do_json_tok_millionths(".", false, 0);
do_json_tok_millionths("..", false, 0);
do_json_tok_millionths("0", true, 0);
do_json_tok_millionths(".0", true, 0);
do_json_tok_millionths("0.", true, 0);
do_json_tok_millionths("100", true, 100 * 1000000);
do_json_tok_millionths("100.0", true, 100 * 1000000);
do_json_tok_millionths("100.", true, 100 * 1000000);
do_json_tok_millionths("100.000001", true, 100 * 1000000 + 1);
do_json_tok_millionths("100.0000001", true, 100 * 1000000);
do_json_tok_millionths(".000009", true, 9);
do_json_tok_millionths(".0000099", true, 9);
do_json_tok_millionths("18446744073709.551615", true,
18446744073709551615ULL);
do_json_tok_millionths("18446744073709.551616", false, 0);
do_json_tok_millionths("18446744073709.551625", false, 0);
do_json_tok_millionths("18446744073709.551715", false, 0);
do_json_tok_millionths("18446744073709.552615", false, 0);
do_json_tok_millionths("18446744073709.561615", false, 0);
do_json_tok_millionths("18446744073709.651615", false, 0);
do_json_tok_millionths("18446744073710.551615", false, 0);
do_json_tok_millionths("18446744073809.551615", false, 0);
do_json_tok_millionths("18446744074709.551615", false, 0);
do_json_tok_millionths("18446744083709.551615", false, 0);
do_json_tok_millionths("18446744173709.551615", false, 0);
do_json_tok_millionths("18446745073709.551615", false, 0);
do_json_tok_millionths("18446754073709.551615", false, 0);
do_json_tok_millionths("18446844073709.551615", false, 0);
do_json_tok_millionths("18447744073709.551615", false, 0);
do_json_tok_millionths("18456744073709.551615", false, 0);
do_json_tok_millionths("18546744073709.551615", false, 0);
do_json_tok_millionths("19446744073709.551615", false, 0);
do_json_tok_millionths("28446744073709.551615", false, 0);
return 0;
}
static void test_json_tok_size(void)
{
const jsmntok_t *toks;
@ -190,6 +246,7 @@ int main(void)
test_json_tok_size();
test_json_tok_bitcoin_amount();
test_json_tok_millionths();
test_json_delve();
assert(!taken_any());
take_cleanup();