IfcParse: read back non-finite REALs serialized with a trailing dot #6409

Legacy releases serialized non-finite doubles by appending a '.' to the
formatted value, producing tokens like "nan.", "-nan(ind)." and "inf."
that strtod stops in front of. The tokenizer then classified them as
schema keywords, and after the #4070 recovery the argument was silently
dropped, leaving zero-argument typed values (e.g. IfcReal()) that raised
cryptic RuntimeErrors from get_psets and ifccsv on any attribute access.

Accept a single trailing '.' after a successful partial strtod parse so
these files load again, and log a parse error for every non-finite REAL
so ifcopenshell.validate surfaces them when validating from a path.
Writing non-finite values remains blocked ("Only finite values are
allowed").

Generated with the assistance of an AI coding tool.
This commit is contained in:
Petru Conduraru
2026-07-20 01:22:56 +03:00
parent b66d8b2c4d
commit 8c91b447f9
2 changed files with 42 additions and 2 deletions
+31 -1
View File
@@ -1,3 +1,5 @@
import math
import ifcopenshell
@@ -21,7 +23,9 @@ END-ISO-10303-21;
def test_reference_to_undefined_owning_instance():
data = "ISO-10303-21;HEADER;FILE_DESCRIPTION();FILE_NAME();FILE_SCHEMA(('IFC4'));#=IFCRELAGGREGATES((#))#5=IFCPOINT)"
data = (
"ISO-10303-21;HEADER;FILE_DESCRIPTION();FILE_NAME();FILE_SCHEMA(('IFC4'));#=IFCRELAGGREGATES((#))#5=IFCPOINT)"
)
ifcopenshell.file.from_string(data)
print(ifcopenshell.get_log())
@@ -30,3 +34,29 @@ def test_reference_to_undefined_owning_instance_simple_type():
data = "ISO-10303-21;HEADER;FILE_DESCRIPTION();FILE_NAME();FILE_SCHEMA(('IFC4'));#=IFCPROJECT((#))#4=IFCSIUNIT("
ifcopenshell.file.from_string(data)
print(ifcopenshell.get_log())
def test_non_finite_reals_with_trailing_dot():
# Legacy serialization appended '.' to non-finite doubles ("nan.", "inf."),
# which tokenized as unknown keywords and left zero-argument typed values
# that raised on attribute access (#6409).
data = """
ISO-10303-21;
HEADER;
FILE_DESCRIPTION((''),'2;1');
FILE_NAME('','',(''),(''),'','','');
FILE_SCHEMA(('IFC2X3'));
ENDSEC;
DATA;
#1=IFCPROPERTYSINGLEVALUE('test',$,IFCREAL(nan.),$);
#2=IFCCARTESIANPOINT((0.,inf.,-inf.));
#3=IFCDIRECTION((-nan.,-nan.,-nan.));
ENDSEC;
END-ISO-10303-21;
"""
f = ifcopenshell.file.from_string(data)
log = ifcopenshell.get_log()
assert math.isnan(f.by_id(1).NominalValue.wrappedValue)
assert f.by_id(2).Coordinates == (0.0, math.inf, -math.inf)
assert all(math.isnan(ratio) for ratio in f.by_id(3).DirectionRatios)
assert "Non-finite value" in log
+11 -1
View File
@@ -30,6 +30,7 @@
#include "utils.h"
#include <algorithm>
#include <cmath>
#include <boost/algorithm/string.hpp>
#include <boost/variant.hpp>
#include <boost/math/special_functions/fpclassify.hpp>
@@ -283,7 +284,13 @@ bool ParseFloat(const char* pStart, double& val) {
double result = strtod_l(pStart, &pEnd, locale);
#endif
if (*pEnd != 0) {
return false;
// Historically REALs were serialized by appending a '.' to any formatted
// double lacking one, producing non-finite spellings such as "nan.",
// "-nan(ind)." and "inf." that strtod stops in front of. Accept a single
// trailing '.' after a successful partial parse so these load again.
if (pEnd == pStart || pEnd[0] != '.' || pEnd[1] != 0) {
return false;
}
}
val = result;
return true;
@@ -339,6 +346,9 @@ Token IfcParse::GeneralTokenPtr(IfcSpfLexer* lexer, size_t start, const std::str
token.type = Token_INT;
} else if (ParseFloat(tokenStr.c_str(), token.value_double)) {
token.type = Token_FLOAT;
if (!std::isfinite(token.value_double)) {
lexer->logger().Message(Logger::LOG_ERROR, "SYN", 41, "Non-finite value '" + tokenStr + "' at offset " + std::to_string(token.startPos));
}
} else {
token.type = Token_KEYWORD;
}