Compare commits

..

4 Commits

Author SHA1 Message Date
Bruno Postle 92f9bd5938 Fix null reference bind in header parsing
references_to_resolve is never set while parsing header
entities, so binding a reference to it was UB, caught by
UBSan on any file with a header.

Generated with the assistance of an AI coding tool.
2026-07-18 00:39:25 +01:00
Andrej730 71a598e63a ifcopenshell.file: small wording fix 2026-07-17 21:55:22 +05:00
Andrej730 3d8115ebc5 ifcopenshell.file: drop workarounds for older builds
Introduced in aeed371 and it's been a while.
2026-07-17 21:55:22 +05:00
Ryan Schultz b5a0f1fc74 Bonsai: allow cross-family class reassignment for spatial elements with geometry (#8665)
The Reassign Class operator refused to reassign an element to a different
IFC product family unless it was an IfcElement <-> IfcElementType swap, so a
piece of geometry mistakenly hosted on IfcSite could not be turned into
IfcFurniture even though root.reassign_class handles it fine.

Loosen the guard: only block the case that actually matters - a spatial
element (IfcSpatialElement / IfcSpatialStructureElement for IFC2X3) with no
geometry, which would be a real containment-hierarchy container rather than
a stray modelled object. Everything else reassigns freely.

Closes #8664

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 18:57:11 -05:00
4 changed files with 30 additions and 26 deletions
+19 -6
View File
@@ -27,6 +27,7 @@ import ifcopenshell.api.material
import ifcopenshell.api.pset
import ifcopenshell.api.root
import ifcopenshell.util.element
import ifcopenshell.util.representation
import ifcopenshell.util.schema
import ifcopenshell.util.shape_builder
import ifcopenshell.util.type
@@ -129,13 +130,25 @@ class ReassignClass(bpy.types.Operator, tool.Ifc.Operator):
same_ifc_product = element.is_a(ifc_product)
if not same_ifc_product:
if not (element.is_a("IfcElement") and ifc_product == "IfcElementType") and not (
element.is_a("IfcElementType") and ifc_product == "IfcElement"
):
self.report(
{"ERROR"}, f"Not supported class reassignment for object '{obj.name}' -> {ifc_product}."
# A spatial element (e.g. IfcSite) anchors the containment
# hierarchy, so only allow reassigning it to another family when
# it actually carries geometry - i.e. it's a real modelled thing
# (a bench dropped onto IfcSite -> IfcFurniture) rather than an
# empty spatial container we'd be turning into a loose element.
# IfcSpatialStructureElement covers IFC2X3, which has no
# IfcSpatialElement supertype.
is_spatial = element.is_a("IfcSpatialElement") or element.is_a("IfcSpatialStructureElement")
if is_spatial:
has_geometry = (
next(ifcopenshell.util.representation.get_representations_iter(element), None) is not None
)
return {"CANCELLED"}
if not has_geometry:
self.report(
{"ERROR"},
f"Cannot reassign '{obj.name}' ({element.is_a()}) to {ifc_product}: "
"a spatial element can only be reassigned to another class when it has geometry.",
)
return {"CANCELLED"}
props = tool.Blender.get_object_bim_props(obj)
props.is_reassigning_class = False
+3 -14
View File
@@ -23,7 +23,6 @@ import numbers
import os
import re
import time
import types
import weakref
import zipfile
from collections.abc import Callable, Generator
@@ -244,19 +243,14 @@ file_dict: dict[int, tuple[weakref.ReferenceType[file], int]] = {}
"""Mapping of internal IfcFile pointer address to existing ``ifcopenshell.file``
and the timestamp when it was created.
Needed only to quickly access related from ``entity_instance`` it's ``file``.
Needed only to quickly access from ``entity_instance`` its ``file``.
"""
READ_ERROR = ifcopenshell_wrapper.file_open_status.READ_ERROR
NO_HEADER = ifcopenshell_wrapper.file_open_status.NO_HEADER
UNSUPPORTED_SCHEMA = ifcopenshell_wrapper.file_open_status.UNSUPPORTED_SCHEMA
INVALID_SYNTAX = ifcopenshell_wrapper.file_open_status.INVALID_SYNTAX
# TODO: Workaround for old builds, remove after build stabilizes.
try:
UNKNOWN = ifcopenshell_wrapper.file_open_status.UNKNOWN
except:
UNKNOWN = 5 # Workaround
UNKNOWN = ifcopenshell_wrapper.file_open_status.UNKNOWN
import struct
@@ -1066,13 +1060,8 @@ class file:
@property
def header(self) -> file_header:
# TODO: Workaround for old builds, remove after build stabilizes.
# TODO: No need for `wrapped_data.header` to be a method - should use `@property`?
header = self.wrapped_data.header
if isinstance(header, types.MethodType):
return file_header(self, self.wrapped_data.header())
else:
return self.wrapped_data.header
return file_header(self, self.wrapped_data.header())
@property
def storage(self) -> Optional[rocksdb_file_storage]:
+1 -5
View File
@@ -61,11 +61,7 @@ namespace {
template <typename Fn>
void dispatch_token(boost::optional<size_t> instance_id, int attribute_id, IfcParse::Token t, IfcParse::declaration* decl, Logger& logger, Fn fn) {
if (t.type == IfcParse::Token_BINARY) {
try {
fn(IfcParse::TokenFunc::asBinary(t));
} catch (IfcParse::IfcException& e) {
logger.Error("VAL", 20, "Invalid binary token at offset " + std::to_string(t.startPos));
}
fn(IfcParse::TokenFunc::asBinary(t));
} else if (IfcParse::TokenFunc::isBool(t)) {
fn(IfcParse::TokenFunc::asBool(t));
} else if (IfcParse::TokenFunc::isLogical(t)) {
+7 -1
View File
@@ -35,7 +35,13 @@ namespace {
parse_context pc;
storage->tokens->Next();
storage->load(-1, nullptr, pc, -1);
return pc.construct(boost::none, *storage->references_to_resolve, decl, decl->as_entity()->attribute_count(), -1, logger);
// references_to_resolve is unset while reading the header (header
// entities such as FILE_DESCRIPTION never reference other
// instances), so fall back to a throwaway list instead of
// dereferencing a null pointer.
unresolved_references no_references;
unresolved_references& references = storage->references_to_resolve ? *storage->references_to_resolve : no_references;
return pc.construct(boost::none, references, decl, decl->as_entity()->attribute_count(), -1, logger);
} else {
// std::unreachable();
return IfcEntityInstanceData(in_memory_attribute_storage(10));