mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-19 22:50:21 +00:00
fix(ifc5d): escape quantity names when serialising Quantities to JSON
serialise_cost_quantities built the "Quantities" JSON string by manual concatenation, inserting quantity.Name and the related element's Name without any escaping. A name containing a double quote, backslash or newline produced invalid JSON, breaking any downstream parser (e.g. a Typst json.decode consumer reporting "failed to parse JSON"). It also crashed with a TypeError when a name was None (str += None). Build a Python list and serialise it with json.dumps instead, keeping the exact same [[name, value], ...] output shape, the element-name prefix and the unsupported-type behaviour. None names are coalesced to "" and quantity values are defensively coerced to float. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -21,6 +21,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import datetime
|
import datetime
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
@@ -256,26 +257,28 @@ class IfcDataGetter:
|
|||||||
return ""
|
return ""
|
||||||
if cost_item.CostQuantities is None:
|
if cost_item.CostQuantities is None:
|
||||||
return ""
|
return ""
|
||||||
string = "["
|
result = []
|
||||||
for quantity in cost_item.CostQuantities:
|
for quantity in cost_item.CostQuantities:
|
||||||
string += '["'
|
prefix = ""
|
||||||
for rel in file.get_inverse(quantity):
|
for rel in file.get_inverse(quantity):
|
||||||
if rel.is_a("IfcPropertySet") or rel.is_a("IfcElementQuantity"):
|
if rel.is_a("IfcPropertySet") or rel.is_a("IfcElementQuantity"):
|
||||||
prop_set = rel
|
|
||||||
# Find elements that have this property set
|
# Find elements that have this property set
|
||||||
for prop_rel in file.get_inverse(prop_set):
|
for prop_rel in file.get_inverse(rel):
|
||||||
if prop_rel.is_a("IfcRelDefinesByProperties"):
|
if prop_rel.is_a("IfcRelDefinesByProperties"):
|
||||||
for obj in prop_rel.RelatedObjects:
|
for obj in prop_rel.RelatedObjects:
|
||||||
if obj.is_a("IfcElement"):
|
if obj.is_a("IfcElement"):
|
||||||
string += obj.Name + " - "
|
prefix += (obj.Name or "") + " - "
|
||||||
string += quantity.Name
|
name = prefix + (quantity.Name or "")
|
||||||
if quantity.is_a("IfcPhysicalSimpleQuantity"):
|
if quantity.is_a("IfcPhysicalSimpleQuantity"):
|
||||||
string += '", ' + str(quantity[3]) + "],"
|
value = quantity[3]
|
||||||
|
try:
|
||||||
|
value = float(value) if value is not None else 0.0
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
value = 0.0
|
||||||
|
result.append([name, value])
|
||||||
else:
|
else:
|
||||||
string += ' ERROR: Only IfcPhysicalSimpleQuantity is supported", 0.0],'
|
result.append([name + " ERROR: Only IfcPhysicalSimpleQuantity is supported", 0.0])
|
||||||
string = string.removesuffix(",")
|
return json.dumps(result, ensure_ascii=False)
|
||||||
string += "]"
|
|
||||||
return string
|
|
||||||
|
|
||||||
|
|
||||||
class SheetData(TypedDict):
|
class SheetData(TypedDict):
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
|
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import csv
|
import csv
|
||||||
|
import json
|
||||||
import tempfile
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -118,3 +119,25 @@ class TestCsv2Ifc:
|
|||||||
writer.write()
|
writer.write()
|
||||||
assert len(list(Path(temp_csv_dir).glob("*.ods"))) == 1
|
assert len(list(Path(temp_csv_dir).glob("*.ods"))) == 1
|
||||||
assert len(list(Path(temp_csv_dir).glob("*.xlsx"))) == 1
|
assert len(list(Path(temp_csv_dir).glob("*.xlsx"))) == 1
|
||||||
|
|
||||||
|
|
||||||
|
class TestSerialiseCostQuantities:
|
||||||
|
def test_quantity_name_with_special_characters_round_trips_as_json(self):
|
||||||
|
ifc_file = ifcopenshell.file()
|
||||||
|
name = 'Prospetto est "Np=256,667-23"'
|
||||||
|
quantity = ifc_file.create_entity("IfcQuantityArea", Name=name, AreaValue=12.5)
|
||||||
|
cost_item = ifc_file.create_entity("IfcCostItem", CostQuantities=[quantity])
|
||||||
|
|
||||||
|
result = ifc5d.ifc5Dspreadsheet.IfcDataGetter.serialise_cost_quantities(ifc_file, cost_item)
|
||||||
|
|
||||||
|
assert json.loads(result) == [[name, 12.5]]
|
||||||
|
|
||||||
|
def test_unset_name_does_not_crash(self):
|
||||||
|
ifc_file = ifcopenshell.file()
|
||||||
|
# Name left unset so quantity.Name resolves to None at access time.
|
||||||
|
quantity = ifc_file.create_entity("IfcQuantityArea", AreaValue=3.0)
|
||||||
|
cost_item = ifc_file.create_entity("IfcCostItem", CostQuantities=[quantity])
|
||||||
|
|
||||||
|
result = ifc5d.ifc5Dspreadsheet.IfcDataGetter.serialise_cost_quantities(ifc_file, cost_item)
|
||||||
|
|
||||||
|
assert json.loads(result) == [["", 3.0]]
|
||||||
|
|||||||
Reference in New Issue
Block a user