Skip to content
43 changes: 37 additions & 6 deletions __init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,27 @@ def build():
yield " " * 8 + "^" * len(d["line"].rstrip())

return "\n".join(build())

class HeaderFieldError(ValidationError):
def __init__(self, field, found_len, expected_len):
self.field = field
self.found_len = found_len
self.expected_len = expected_len

def asdict(self, with_message=True):
return {
"type": "invalid_header_field",
"field": self.field,
"expected_field_count": self.expected_len,
"actual_field_count": self.found_len,
**({"message": str(self)} if with_message else {}),
}

def __str__(self):
return (
f"Invalid number of parameters for HEADER field '{self.field}'. "
f"Expected {self.expected_len}, found {self.found_len}."
)


grammar = r"""
Expand Down Expand Up @@ -184,6 +205,12 @@ def build():
%ignore /[ \t\f\r\n]/+
"""

HEADER_FIELDS = {
"file_description": namedtuple('file_description', ['description', 'implementation_level']),
"file_name": namedtuple('file_name', ['name', 'time_stamp', 'author', 'organization', 'preprocessor_version', 'originating_system', 'authorization']),
"file_schema": namedtuple('file_schema', ['schema_identifiers']),
}


class Ref:
def __init__(self, id):
Expand Down Expand Up @@ -304,6 +331,11 @@ def process_tree(filecontent, file_tree, with_progress, with_header=False):

if with_header:
header = dict(map(make_header_ent, header.children[0].children))
for field in HEADER_FIELDS.keys():
observed = header.get(field.upper(), [])
expected = HEADER_FIELDS.get(field)._fields
if len(header.get(field.upper(), [])) != len(expected):
raise HeaderFieldError(field.upper(), len(observed), len(expected))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to really raise an exception and terminate or wait for potentially additional errors?


n = len(data.children)
if n:
Expand Down Expand Up @@ -373,6 +405,11 @@ def replace_fn(match):
header_tree = ast.children[0] # HEADER section

header = dict(map(make_header_ent, header_tree.children[0].children))
for field in HEADER_FIELDS.keys():
observed = header.get(field.upper(), [])
expected = HEADER_FIELDS.get(field)._fields
if len(header.get(field.upper(), [])) != len(expected):
raise HeaderFieldError(field.upper(), len(observed), len(expected))
return header


Expand Down Expand Up @@ -473,13 +510,7 @@ def schema_version(self) -> tuple[int, int, int, int]:

@property
def header(self):
HEADER_FIELDS = {
"file_description": namedtuple('file_description', ['description', 'implementation_level']),
"file_name": namedtuple('file_name', ['name', 'time_stamp', 'author', 'organization', 'preprocessor_version', 'originating_system', 'authorization']),
"file_schema": namedtuple('file_schema', ['schema_identifiers']),
}
header = {}

for field_name, namedtuple_class in HEADER_FIELDS.items():
field_data = self.header_.get(field_name.upper(), [])
header[field_name.lower()] = namedtuple_class(*field_data)
Expand Down
30 changes: 30 additions & 0 deletions fixtures/too_many_header_entity_fields.ifc
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
ISO-10303-21;
HEADER;
FILE_DESCRIPTION(('ViewDefinition [ReferenceView_V1.2]', 'ExchangeRequirement [Any]'),'2;1');
FILE_NAME('Header.ifc','2025-02-13T15:58:45',('tricott'),('Trimble Inc.'),'TrimBimToIFC rel. 4.0.2','Example - Example - 2025.0','IFC4 model', '');
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#1=IFCPERSON($,$,'',$,$,$,$,$);
#2=IFCORGANIZATION($,'',$,$,$);
#3=IFCPERSONANDORGANIZATION(#1,#2,$);
#4=IFCAPPLICATION(#2,'v0.7.0-6c9e130ca','IfcOpenShell-v0.7.0-6c9e130ca','');
#5=IFCOWNERHISTORY(#3,#4,$,.NOTDEFINED.,$,#3,#4,1700419055);
#6=IFCDIRECTION((1.,0.,0.));
#7=IFCDIRECTION((0.,0.,1.));
#8=IFCCARTESIANPOINT((0.,0.,0.));
#9=IFCAXIS2PLACEMENT3D(#8,#7,#6);
#10=IFCDIRECTION((0.,1.));
#11=IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,1.E-05,#9,#10);
#12=IFCDIMENSIONALEXPONENTS(0,0,0,0,0,0,0);
#13=IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);
#14=IFCSIUNIT(*,.AREAUNIT.,$,.SQUARE_METRE.);
#15=IFCSIUNIT(*,.VOLUMEUNIT.,$,.CUBIC_METRE.);
#16=IFCSIUNIT(*,.PLANEANGLEUNIT.,$,.RADIAN.);
#17=IFCMEASUREWITHUNIT(IFCPLANEANGLEMEASURE(0.017453292519943295),#16);
#18=IFCCONVERSIONBASEDUNIT(#12,.PLANEANGLEUNIT.,'DEGREE',#17);
#19=IFCUNITASSIGNMENT((#13,#14,#15,#18));
#20=IFCPROJECT('0iDmeiiLP3AOllitM2Favn',#5,'',$,$,$,$,(#11),#19);
#21=IFCSITE('3rg2jGkIH10RFhrQsGZKRk',#5,$,$,$,$,$,$,$,$,$,$,$,$);
ENDSEC;
END-ISO-10303-21;
9 changes: 9 additions & 0 deletions test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,3 +126,12 @@ def test_valid_headers(filename):
# error in body; with_header should not raise an error
with nullcontext():
parse(filename=filename, with_tree=False, only_header=True, with_header=True)

def test_too_many_header_entity_fields():
with pytest.raises(ValidationError):
parse(filename='fixtures/too_many_header_entity_fields.ifc', only_header=True)

def test_too_many_header_entity_fields_whole_file():
with pytest.raises(ValidationError):
parse(filename='fixtures/too_many_header_entity_fields.ifc', with_header=True)