From 87bc6bfbaba993f255b17607b3cdf34638350a57 Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Sat, 22 Aug 2026 13:12:36 +0200 Subject: [PATCH] Add string decode/encode api --- .../ifcopenshell/__init__.py | 4 ++++ .../ifcopenshell/ifcopenshell_wrapper.pyi | 2 ++ src/ifcopenshell-python/test/test_parse.py | 8 ++++++++ src/ifcparse/character_decoder.h | 6 +----- src/ifcparse/parse.cpp | 20 +++++++++++++++++++ src/ifcparse/parse.h | 4 ++++ .../tests/test_ifcopenshell_parse.cpp | 10 ++++++++++ src/ifcwrap/IfcParseWrapper.i | 6 ++++++ 8 files changed, 55 insertions(+), 5 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/__init__.py b/src/ifcopenshell-python/ifcopenshell/__init__.py index 6e4731a35d..b0f52f654d 100644 --- a/src/ifcopenshell-python/ifcopenshell/__init__.py +++ b/src/ifcopenshell-python/ifcopenshell/__init__.py @@ -95,6 +95,8 @@ from .sql import sqlite, sqlite_entity rocksdb_lazy_instance = file_module.rocksdb_lazy_instance +decode_spf_string = ifcopenshell_wrapper.decode_spf_string +encode_spf_string = ifcopenshell_wrapper.encode_spf_string get_log = ifcopenshell_wrapper.get_log logger = ifcopenshell_wrapper.logger if hasattr(ifcopenshell_wrapper, "logger") else None if hasattr(ifcopenshell_wrapper, "logger_or_root"): @@ -114,6 +116,8 @@ def optional_logger_args(logger: ifcopenshell_wrapper.logger | None) -> tuple[lo # (it's a requirement for a typed library) __all__ = [ "clear_plugin_search_paths", + "decode_spf_string", + "encode_spf_string", "entity_instance", "file", "get_plugin_search_paths", diff --git a/src/ifcopenshell-python/ifcopenshell/ifcopenshell_wrapper.pyi b/src/ifcopenshell-python/ifcopenshell/ifcopenshell_wrapper.pyi index c233254204..c6ccd5cdd8 100644 --- a/src/ifcopenshell-python/ifcopenshell/ifcopenshell_wrapper.pyi +++ b/src/ifcopenshell-python/ifcopenshell/ifcopenshell_wrapper.pyi @@ -1649,6 +1649,8 @@ def construct_iterator_with_include_exclude_id( geometry_library, settings, file, elems, include, num_threads, logger=None ): ... def convert_loop_to_function_item(loop): ... +def decode_spf_string(value: str) -> str: ... +def encode_spf_string(value: str) -> str: ... class attribute_value_derived: ... diff --git a/src/ifcopenshell-python/test/test_parse.py b/src/ifcopenshell-python/test/test_parse.py index d40101333e..90779c6df8 100644 --- a/src/ifcopenshell-python/test/test_parse.py +++ b/src/ifcopenshell-python/test/test_parse.py @@ -1,6 +1,14 @@ import ifcopenshell +def test_spf_strings_can_be_encoded_and_decoded(): + decoded = "Café's \\" + encoded = r"'Caf\X2\00E9\X0\''s \\'" + + assert ifcopenshell.encode_spf_string(decoded) == encoded + assert ifcopenshell.decode_spf_string(encoded) == decoded + + def test_skip_over_non_entity_instance(): data = """ ISO-10303-21; diff --git a/src/ifcparse/character_decoder.h b/src/ifcparse/character_decoder.h index 3426d92af7..31096a19b5 100644 --- a/src/ifcparse/character_decoder.h +++ b/src/ifcparse/character_decoder.h @@ -67,10 +67,6 @@ class IFC_PARSE_API character_decoder { std::string get(size_t& offset); }; -} // namespace ifcopenshell - -namespace ifcopenshell { - class IFC_PARSE_API character_encoder { private: std::u32string str_; @@ -80,6 +76,6 @@ class IFC_PARSE_API character_encoder { operator std::string(); }; -} // namespace IfcWrite +} // namespace ifcopenshell #endif diff --git a/src/ifcparse/parse.cpp b/src/ifcparse/parse.cpp index eb3a0459e9..a775118817 100644 --- a/src/ifcparse/parse.cpp +++ b/src/ifcparse/parse.cpp @@ -550,6 +550,26 @@ std::string token::to_string() { return result; } +std::string ifcopenshell::encode_spf_string(const std::string& value) { + return character_encoder(value); +} + +std::string ifcopenshell::decode_spf_string(const std::string& value) { + std::string wrapped; + auto value_p = &value; + if (!value.empty() && value.front() != '\'') { + wrapped = "'" + value + "'"; + value_p = &wrapped; + } + file_reader reader(*value_p, caller_fed_tag{}); + spf_lexer> lexer(&reader); + token decoded = lexer.next(); + if (!decoded.is_string()) { + throw exception("Expected an SPF string"); + } + return decoded.as_string(); +} + namespace { template diff --git a/src/ifcparse/parse.h b/src/ifcparse/parse.h index 8456cfbd57..5986299e0e 100644 --- a/src/ifcparse/parse.h +++ b/src/ifcparse/parse.h @@ -47,6 +47,10 @@ extern IFC_PARSE_API const char *IFCOPENSHELL_VERSION; namespace ifcopenshell { +IFC_PARSE_API std::string encode_spf_string(const std::string& value); + +IFC_PARSE_API std::string decode_spf_string(const std::string& value); + /// A stream of tokens to be read from a file_reader. template class IFC_PARSE_API spf_lexer { diff --git a/src/ifcparse/tests/test_ifcopenshell_parse.cpp b/src/ifcparse/tests/test_ifcopenshell_parse.cpp index d0f9c0c2ff..760d3aca3a 100644 --- a/src/ifcparse/tests/test_ifcopenshell_parse.cpp +++ b/src/ifcparse/tests/test_ifcopenshell_parse.cpp @@ -2,9 +2,19 @@ #include #include +#include #include #include +TEST_CASE("SPF strings can be encoded and decoded", "[ifcparse]") { + const std::string decoded = "Caf\xC3\xA9" "'s \\"; + const std::string encoded = R"('Caf\X2\00E9\X0\''s \\')"; + + CHECK(ifcopenshell::encode_spf_string(decoded) == encoded); + CHECK(ifcopenshell::decode_spf_string(encoded) == decoded); + CHECK(ifcopenshell::decode_spf_string(encoded.substr(1, encoded.size() - 2)) == decoded); +} + TEST_CASE("IfcPropertySetDefinitionSet references are resolved without replacing their owner", "[ifcparse]") { const std::string fixture = std::string(IFCOPENSHELL_TEST_FIXTURES) + "/ColumnPSetsOfSets.ifc"; ifcopenshell::file file(fixture); diff --git a/src/ifcwrap/IfcParseWrapper.i b/src/ifcwrap/IfcParseWrapper.i index 42c22bd07f..63f5fc8ba6 100644 --- a/src/ifcwrap/IfcParseWrapper.i +++ b/src/ifcwrap/IfcParseWrapper.i @@ -962,6 +962,12 @@ private: }; %include "../ifcparse/ifc_parse_api.h" + +namespace ifcopenshell { +std::string encode_spf_string(const std::string& value); +std::string decode_spf_string(const std::string& value); +} + %include "../ifcparse/spf_header.h" %pythoncode %{