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
@@ -1,8 +1,11 @@
// This file was generated with the assistance of an AI coding tool.
#include <catch2/catch_test_macros.hpp>
#include <ifcparse/exception.h>
#include <ifcparse/file.h>
#include <ifcparse/parse.h>
#include <cstdint>
#include <sstream>
#include <string>
#include <vector>
@@ -97,6 +100,37 @@ TEST_CASE("Aggregate inverse updates preserve reference multiplicity", "[ifcpars
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]") {
ifcopenshell::file file(ifcopenshell::schema_by_name("IFC4"));
const auto* point_declaration = file.schema()->declaration_by_name("IfcCartesianPoint");