Skip to content
Merged
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
8 changes: 8 additions & 0 deletions changes/256.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Corrected handling of special float values (.nan, .inf, etc.) in the YAML tree

Previously scalars were just passed through ``strtod`` to check if they were
value float scalars but this is incorrect for the YAML core schema which does
not parse bare (unquoted) "inf" or "nan" as floats, but rather as strings.

This corrects handling of these values in-line with what PyYAML does, as well
as what the YAML core schema documents.
55 changes: 54 additions & 1 deletion src/value.c
Original file line number Diff line number Diff line change
Expand Up @@ -1736,12 +1736,57 @@ static asdf_value_err_t is_yaml_unsigned_int(
}


/**
* Parse the YAML 1.1 special float values ``[-+]?.inf`` and ``.nan``
*
* Only the lowercase, titlecase, and uppercase spellings are allowed, and
* NaN may not be signed.
*/
static bool is_yaml_special_float(const char *scalar, size_t len, double *value) {
char sign = '\0';

if (len == 5 && (scalar[0] == '-' || scalar[0] == '+')) {
sign = scalar[0];
scalar++;
len--;
}

if (len != 4)
return false;

if ((0 == strncmp(scalar, ".inf", len)) || (0 == strncmp(scalar, ".Inf", len)) ||
(0 == strncmp(scalar, ".INF", len))) {
*value = (sign == '-') ? -INFINITY : INFINITY;
return true;
}

if (sign)
return false;

if ((0 == strncmp(scalar, ".nan", len)) || (0 == strncmp(scalar, ".NaN", len)) ||
(0 == strncmp(scalar, ".NAN", len))) {
*value = NAN;
return true;
}

return false;
}


static asdf_value_err_t is_yaml_float(
const char *scalar, size_t len, double *value, asdf_value_type_t *type) {

if (!scalar)
return ASDF_VALUE_ERR_UNKNOWN;

/* strtod also accepts bare inf, nan, and infinity, which YAML 1.1 (and
* PyYAML) treat as strings, so only hand it scalars that start with a
* digit or '.' after an optional sign */
size_t start = (len > 0 && (scalar[0] == '-' || scalar[0] == '+')) ? 1 : 0;

if (start >= len || !(isdigit((unsigned char)scalar[start]) || scalar[start] == '.'))
return ASDF_VALUE_ERR_PARSE_FAILURE;

char *double_s = strndup(scalar, len);

if (!double_s)
Expand All @@ -1759,6 +1804,13 @@ static asdf_value_err_t is_yaml_float(

if (errno || *end) {
free(double_s);

/* Less common, so only checked after strtod fails */
if (is_yaml_special_float(scalar, len, value)) {
*type = ASDF_VALUE_DOUBLE;
return ASDF_VALUE_OK;
}

return ASDF_VALUE_ERR_PARSE_FAILURE;
}

Expand Down Expand Up @@ -2687,7 +2739,8 @@ asdf_value_err_t asdf_value_as_float(asdf_value_t *value, float *out) {
if (LIKELY(out))
*out = flt;

if (!isfinite(flt))
/* Only an overflow if a finite double became non-finite (not .inf/.nan) */
if (!isfinite(flt) && isfinite(value->scalar.d))
return ASDF_VALUE_ERR_OVERFLOW;

return ASDF_VALUE_OK;
Expand Down
19 changes: 19 additions & 0 deletions tests/fixtures/scalars.asdf
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,23 @@ bigint: 18446744073709551616
float32: 0.15625
float64: 1.000000059604644775390625
bigfloat: 1e400
inf: .inf
Inf: .Inf
INF: .INF
pos_inf: +.inf
neg_inf: -.inf
neg_Inf: -.Inf
neg_INF: -.INF
nan: .nan
NaN: .NaN
NAN: .NAN
bad_inf: .iNf
neg_nan: -.nan
pos_nan: +.nan
bare_inf: inf
bare_Inf: Inf
bare_neg_inf: -inf
bare_infinity: infinity
bare_nan: nan
bare_NaN: NaN
...
43 changes: 41 additions & 2 deletions tests/test-value.c
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include <float.h>
#include <math.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
Expand Down Expand Up @@ -61,6 +62,26 @@ MU_TEST(test_asdf_value_get_type) {
CHECK_VALUE_TYPE("float32", ASDF_VALUE_DOUBLE);
CHECK_VALUE_TYPE("float64", ASDF_VALUE_DOUBLE);
CHECK_VALUE_TYPE("bigfloat", ASDF_VALUE_DOUBLE);
CHECK_VALUE_TYPE("inf", ASDF_VALUE_DOUBLE);
CHECK_VALUE_TYPE("Inf", ASDF_VALUE_DOUBLE);
CHECK_VALUE_TYPE("INF", ASDF_VALUE_DOUBLE);
CHECK_VALUE_TYPE("pos_inf", ASDF_VALUE_DOUBLE);
CHECK_VALUE_TYPE("neg_inf", ASDF_VALUE_DOUBLE);
CHECK_VALUE_TYPE("neg_Inf", ASDF_VALUE_DOUBLE);
CHECK_VALUE_TYPE("neg_INF", ASDF_VALUE_DOUBLE);
CHECK_VALUE_TYPE("nan", ASDF_VALUE_DOUBLE);
CHECK_VALUE_TYPE("NaN", ASDF_VALUE_DOUBLE);
CHECK_VALUE_TYPE("NAN", ASDF_VALUE_DOUBLE);
// Not valid YAML 1.1 float spellings; plain strings (same as PyYAML)
CHECK_VALUE_TYPE("bad_inf", ASDF_VALUE_STRING);
CHECK_VALUE_TYPE("neg_nan", ASDF_VALUE_STRING);
CHECK_VALUE_TYPE("pos_nan", ASDF_VALUE_STRING);
CHECK_VALUE_TYPE("bare_inf", ASDF_VALUE_STRING);
CHECK_VALUE_TYPE("bare_Inf", ASDF_VALUE_STRING);
CHECK_VALUE_TYPE("bare_neg_inf", ASDF_VALUE_STRING);
CHECK_VALUE_TYPE("bare_infinity", ASDF_VALUE_STRING);
CHECK_VALUE_TYPE("bare_nan", ASDF_VALUE_STRING);
CHECK_VALUE_TYPE("bare_NaN", ASDF_VALUE_STRING);
asdf_close(file);
return MUNIT_OK;
}
Expand Down Expand Up @@ -753,10 +774,16 @@ MU_TEST(test_asdf_value_as_float) {
assert_not_null(file);
CHECK_FLOAT_VALUE(float, "float32", ASDF_VALUE_OK, 0.15625);
CHECK_FLOAT_VALUE(float, "float64", ASDF_VALUE_OK, 1.000000059604644775390625);
CHECK_FLOAT_VALUE(float, "inf", ASDF_VALUE_OK, INFINITY);
CHECK_FLOAT_VALUE(float, "pos_inf", ASDF_VALUE_OK, INFINITY);
CHECK_FLOAT_VALUE(float, "neg_INF", ASDF_VALUE_OK, -INFINITY);
CHECK_FLOAT_VALUE_MISMATCH(float, "plain");
CHECK_FLOAT_VALUE_MISMATCH(float, "");
asdf_value_t *float_val = asdf_value_of_float(file, 1.0F);
CHECK_FLOAT_VALUE_MISMATCH(float, "bad_inf");
float out = 0.0F;
assert_int(asdf_get_float(file, "NaN", &out), ==, ASDF_VALUE_OK);
assert_true(isnan(out));
asdf_value_t *float_val = asdf_value_of_float(file, 1.0F);
assert_int(asdf_value_as_float(float_val, &out), ==, ASDF_VALUE_OK);
assert_float(out, ==, 1.0F);
asdf_value_destroy(float_val);
Expand All @@ -781,11 +808,23 @@ MU_TEST(test_asdf_value_as_double) {
assert_not_null(file);
CHECK_FLOAT_VALUE(double, "float32", ASDF_VALUE_OK, 0.15625);
CHECK_FLOAT_VALUE(double, "float64", ASDF_VALUE_OK, 1.000000059604644775390625);
CHECK_FLOAT_VALUE(double, "Inf", ASDF_VALUE_OK, INFINITY);
CHECK_FLOAT_VALUE(double, "INF", ASDF_VALUE_OK, INFINITY);
CHECK_FLOAT_VALUE(double, "neg_inf", ASDF_VALUE_OK, -INFINITY);
CHECK_FLOAT_VALUE(double, "neg_Inf", ASDF_VALUE_OK, -INFINITY);
CHECK_FLOAT_VALUE_MISMATCH(double, "plain");
CHECK_FLOAT_VALUE_MISMATCH(double, "");
CHECK_FLOAT_VALUE_MISMATCH(double, "neg_nan");
CHECK_FLOAT_VALUE_MISMATCH(double, "bare_inf");
CHECK_FLOAT_VALUE_MISMATCH(double, "bare_nan");

asdf_value_t *float_val = asdf_value_of_float(file, 1.0F);
double out = 0.0;
assert_int(asdf_get_double(file, "nan", &out), ==, ASDF_VALUE_OK);
assert_true(isnan(out));
assert_int(asdf_get_double(file, "NAN", &out), ==, ASDF_VALUE_OK);
assert_true(isnan(out));

asdf_value_t *float_val = asdf_value_of_float(file, 1.0F);
assert_int(asdf_value_as_double(float_val, &out), ==, ASDF_VALUE_OK);
assert_double(out, ==, 1.0);
asdf_value_destroy(float_val);
Expand Down