Fix to_string() on eof marker token (#9463)

* Rewrite to_string() to use switch{} and handle Token_NONE and identifier without as_string()

* Add regression tests for to_string() on tokens without a string form

Cover both halves of the recursion that made a whitespace-only file
segfault: token::to_string() on the EOF marker and on an instance name,
and a parse of input that lexes to zero tokens, which is how the header
parser reaches token::as_string() on the EOF marker.

Generated with the assistance of an AI coding tool.

---------

Co-authored-by: Bruno Postle <bruno@postle.net>
This commit is contained in:
Thomas Krijnen
2026-09-10 11:02:00 +02:00
committed by GitHub
parent e1be433207
commit 148ff02859
2 changed files with 59 additions and 10 deletions
+25 -10
View File
@@ -536,19 +536,34 @@ boost::dynamic_bitset<> token::as_binary() {
} }
std::string token::to_string() { std::string token::to_string() {
std::string result; switch (type) {
if (type == Token_OPERATOR || type == Token_BOOL) { case Token_OPERATOR:
result.push_back(value_char); case Token_BOOL:
} else if (type == Token_INT) { return std::string(1, value_char);
result = std::to_string(value_int);
} else if (type == Token_FLOAT) { case Token_INT:
return std::to_string(value_int);
case Token_IDENTIFIER:
return "#" + std::to_string(value_int);
case Token_FLOAT: {
std::ostringstream oss; std::ostringstream oss;
oss << std::setprecision(15) << value_double; oss << std::setprecision(15) << value_double;
result = oss.str(); return oss.str();
} else {
return as_string();
} }
return result;
case Token_STRING:
case Token_ENUMERATION:
case Token_BINARY:
case Token_KEYWORD:
return as_string();
case Token_NONE:
throw invalid_token_exception(start_pos, "", "");
}
throw exception("Unknown token type");
} }
std::string ifcopenshell::encode_spf_string(const std::string& value) { std::string ifcopenshell::encode_spf_string(const std::string& value) {
@@ -1,8 +1,11 @@
// This file was generated with the assistance of an AI coding tool. // This file was generated with the assistance of an AI coding tool.
#include <catch2/catch_test_macros.hpp> #include <catch2/catch_test_macros.hpp>
#include <ifcparse/exception.h>
#include <ifcparse/file.h> #include <ifcparse/file.h>
#include <ifcparse/parse.h> #include <ifcparse/parse.h>
#include <cstdint>
#include <sstream>
#include <string> #include <string>
#include <vector> #include <vector>
@@ -97,6 +100,37 @@ TEST_CASE("Aggregate inverse updates preserve reference multiplicity", "[ifcpars
CHECK(inverse_count(segment_d) == 1); CHECK(inverse_count(segment_d) == 1);
} }
TEST_CASE("Tokens without a string representation do not recurse in to_string()", "[ifcparse]") {
// to_string() used to delegate to as_string() for every token type it did
// not handle explicitly, while as_string() builds its exception message
// with to_string(). An EOF marker or an instance name therefore recursed
// between the two until the stack was exhausted.
ifcopenshell::token eof;
REQUIRE(eof.type == ifcopenshell::token::Token_NONE);
CHECK_THROWS_AS(eof.to_string(), ifcopenshell::invalid_token_exception);
CHECK_THROWS_AS(eof.as_string(), ifcopenshell::invalid_token_exception);
ifcopenshell::token identifier(0, ifcopenshell::token::Token_IDENTIFIER, (int64_t)123);
CHECK(identifier.to_string() == "#123");
CHECK_THROWS_AS(identifier.as_string(), ifcopenshell::invalid_token_exception);
}
TEST_CASE("Files that contain no tokens are rejected rather than crashing", "[ifcparse]") {
// The header parser asks the lexer for a keyword before checking for EOF,
// so input that lexes to zero tokens reaches token::as_string() on the EOF
// marker. Parsing must fail cleanly instead of overflowing the stack.
const std::vector<std::string> inputs{" ", "\r\n\t ", "/* only a comment */"};
for (const auto& contents : inputs) {
INFO("input: " << contents);
ifcopenshell::logger log;
std::istringstream input(contents);
ifcopenshell::file file(input, (int)contents.size(), log);
CHECK(file.good().value() != ifcopenshell::file_open_status::SUCCESS);
}
}
TEST_CASE("Inverse lookups stay consistent across interleaved adds, removals and reads", "[ifcparse]") { TEST_CASE("Inverse lookups stay consistent across interleaved adds, removals and reads", "[ifcparse]") {
ifcopenshell::file file(ifcopenshell::schema_by_name("IFC4")); ifcopenshell::file file(ifcopenshell::schema_by_name("IFC4"));
const auto* point_declaration = file.schema()->declaration_by_name("IfcCartesianPoint"); const auto* point_declaration = file.schema()->declaration_by_name("IfcCartesianPoint");