diff --git a/changes/256.bugfix b/changes/256.bugfix new file mode 100644 index 00000000..df8dfd0b --- /dev/null +++ b/changes/256.bugfix @@ -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. diff --git a/src/value.c b/src/value.c index d054bc9a..8173ec0b 100644 --- a/src/value.c +++ b/src/value.c @@ -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) @@ -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; } @@ -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; diff --git a/tests/fixtures/scalars.asdf b/tests/fixtures/scalars.asdf index 09c03d7e..213f46b8 100644 --- a/tests/fixtures/scalars.asdf +++ b/tests/fixtures/scalars.asdf @@ -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 ... diff --git a/tests/test-value.c b/tests/test-value.c index 4f17653d..fd40cb10 100644 --- a/tests/test-value.c +++ b/tests/test-value.c @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -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; } @@ -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); @@ -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);