Skip to content

Commit 07c6f08

Browse files
committed
feat: add Canonical EXI encoding support for ISO 15118-2/-20 Plug & Charge
Add opt-in Canonical EXI code generation (canonical_exi_enabled config flag) for ISO 15118-2 and ISO 15118-20 Plug & Charge XML signature workflows. When enabled, the generator produces a 2-mode runtime system: - mode=0 (EXI_MODE_STANDARD): schema-informed standard EXI (0x80 header, bit-packed) - mode=1 (EXI_MODE_CANONICAL): Canonical EXI for Plug & Charge (0x80 header, bit-packed, type-aware encoding per W3C Canonical EXI specification) Key changes: - Runtime canonical_mode field in exi_bitstream_t struct, set by caller - W3C-compliant canonical attribute ordering (xsi:type, xsi:nil, then lexicographic by local_name + namespace_uri) - Recursive schema import processing for complete fragment grammar discovery - String table partition prefix encoding for canonical string values - base64Binary decoder fix for correct nesting depth - New error codes for canonical decoding validation - exi_types_encoder.h included in all encoder configurations The feature is disabled by default (canonical_exi_enabled = 0). Set to 1 in config.py to generate canonical EXI code paths. Validated against EXIficient reference implementation (8/8 fragment digest match, RSA-SHA512 + ECDSA-SHA256 signature verification) and Keysight SL1550A EVCC live hardware (byte-identical 2,247-byte canonical output). Signed-off-by: jharper <jharper@anl.gov>
1 parent afd732d commit 07c6f08

16 files changed

Lines changed: 212 additions & 30 deletions

src/cbexigen/FileGenerator.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,8 @@ def __generate_static_h(self, parameters):
113113
generator = tools_generator.get_generator()
114114
temp = generator.get_template(config['template'])
115115
code = temp.render(filename=config['filename'], filekey=config['identifier'],
116-
add_debug_code=self.__analyzer_data.add_debug_code_enabled)
116+
add_debug_code=self.__analyzer_data.add_debug_code_enabled,
117+
canonical_exi_enabled=tools_conf.CONFIG_PARAMS['canonical_exi_enabled'])
117118

118119
tools.save_code_to_file(config['filename'], code, parameters['folder'])
119120
except KeyError as err:
@@ -131,7 +132,8 @@ def __generate_static_c(self, parameters):
131132
generator = tools_generator.get_generator()
132133
temp = generator.get_template(config['template'])
133134
code = temp.render(filename=config['filename'], filekey=config['identifier'],
134-
add_debug_code=self.__analyzer_data.add_debug_code_enabled)
135+
add_debug_code=self.__analyzer_data.add_debug_code_enabled,
136+
canonical_exi_enabled=tools_conf.CONFIG_PARAMS['canonical_exi_enabled'])
135137

136138
tools.save_code_to_file(config['filename'], code, parameters['folder'])
137139
except KeyError as err:

src/cbexigen/SchemaAnalyzer.py

Lines changed: 60 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# SPDX-License-Identifier: Apache-2.0
22
# Copyright (c) 2022 - 2023 chargebyte GmbH
33
# Copyright (c) 2022 - 2023 Contributors to EVerest
4+
import sys
45
from typing import Union
56

67
from xmlschema import XMLSchema11, XsdElement, XsdType, XsdAttribute
@@ -15,6 +16,22 @@
1516
from cbexigen.tools_config import CONFIG_PARAMS, get_config_module
1617

1718

19+
def _canonical_attribute_sort_key(particle):
20+
"""Sort key for canonical EXI attribute ordering per W3C Canonical EXI spec.
21+
22+
Order: xsi:type first, xsi:nil second, then lexicographic by (local_name, namespace_uri).
23+
Uses Unicode code point comparison (Python's default string comparison).
24+
"""
25+
XSI_NAMESPACE = 'http://www.w3.org/2001/XMLSchema-instance'
26+
27+
if particle.namespace_uri == XSI_NAMESPACE and particle.name == 'type':
28+
return (0, '', '') # sort first
29+
elif particle.namespace_uri == XSI_NAMESPACE and particle.name == 'nil':
30+
return (1, '', '') # sort second
31+
else:
32+
return (2, particle.name, particle.namespace_uri) # lexicographic by (local_name, ns_uri)
33+
34+
1835
class SchemaAnalyzer(object):
1936

2037
def __init__(self, schema, schema_base, analyzer_data: AnalyzerData, schema_prefix):
@@ -336,6 +353,10 @@ def __get_particle_from_attribute(self, attribute: XsdAttribute):
336353

337354
particle.is_attribute = True
338355

356+
# Store namespace URI for canonical EXI attribute ordering.
357+
# XsdAttribute inherits target_namespace from XsdComponent.
358+
particle.namespace_uri = attribute.target_namespace or ''
359+
339360
if attribute.use.casefold() == 'required':
340361
particle.min_occurs = 1
341362
particle.max_occurs = 1
@@ -788,7 +809,10 @@ def __get_element_data(self, element: XsdElement, level, count, subst_list):
788809
temp_list.append(self.__get_particle_from_attribute(attribute))
789810

790811
if len(temp_list) > 1:
791-
temp_list.sort(key=lambda item: item.name, reverse=False)
812+
if CONFIG_PARAMS.get('canonical_exi_enabled', 0) == 1:
813+
temp_list.sort(key=_canonical_attribute_sort_key, reverse=False)
814+
else:
815+
temp_list.sort(key=lambda item: item.name, reverse=False)
792816
element_data.particles.extend(temp_list)
793817

794818
if element.type.content_type_label == 'simple':
@@ -1080,25 +1104,51 @@ def __print_child_recursive(element_list, child_element: XsdElement):
10801104

10811105
# There are unused elements in the ISO-20 schema that are not yet included in the list of all elements
10821106
# for the fragment decoder and encoder. These elements can be determined via the components.
1083-
# Therefore, we iterate through the components of the schema and the 1st level of imports and complete the list.
1084-
# TODO: As only ISO-20 is currently affected and the only import of the individual schemas is the
1085-
# CommonTypes schema, recursive processing is not used here. This should be changed if necessary.
1107+
# Therefore, we iterate through the components of the schema and ALL transitive imports recursively.
1108+
# This matches EXIficient's fragment grammar construction which includes all imported schema elements.
10861109
for component in self.__current_schema.iter_components():
10871110
if isinstance(component, Xsd11Element):
10881111
if component.name not in fragments.keys():
10891112
fragments[component.name] = __get_fragment(component)
10901113

1091-
for import_item in self.__current_schema.imports.values():
1092-
imported_schema = XMLSchema11(import_item.name, base_url=self.__schema_base, build=True)
1093-
for component in imported_schema.iter_components():
1094-
if isinstance(component, Xsd11Element):
1095-
if component.name not in fragments.keys():
1096-
fragments[component.name] = __get_fragment(component)
1114+
# Recursive helper to process all transitive imports
1115+
def process_imports(schema, processed_schemas=None):
1116+
if processed_schemas is None:
1117+
processed_schemas = set()
1118+
1119+
for import_item in schema.imports.values():
1120+
# Avoid infinite loops in case of circular imports
1121+
import_path = import_item.name
1122+
if import_path in processed_schemas:
1123+
continue
1124+
processed_schemas.add(import_path)
1125+
1126+
imported_schema = XMLSchema11(import_path, base_url=self.__schema_base, build=True)
1127+
1128+
# Add elements from this imported schema
1129+
for component in imported_schema.iter_components():
1130+
if isinstance(component, Xsd11Element):
1131+
if component.name not in fragments.keys():
1132+
fragments[component.name] = __get_fragment(component)
1133+
1134+
# Recursively process this schema's imports
1135+
process_imports(imported_schema, processed_schemas)
1136+
1137+
# Process all imports recursively (levels 1, 2, 3, ...)
1138+
process_imports(self.__current_schema)
10971139

10981140
# Sort the list of elements and types by 1. name and 2. namespace
10991141
sorted_by_name = dict(sorted(fragments.items(), key=lambda item: (item[1].name, item[1].namespace)))
11001142
self.__known_fragments.update(sorted_by_name)
11011143

1144+
# Debug logging for fragment ordering (gated by environment variable)
1145+
import os
1146+
if os.environ.get('CBEXIGEN_DEBUG_FRAGMENTS') == '1':
1147+
print("\n=== cbexigen Fragment Grammar Debug ===", file=sys.stderr)
1148+
for index, (qname, fragment) in enumerate(sorted_by_name.items()):
1149+
print(f"FRAGMENT[{index}]: {fragment.name} | {fragment.namespace}", file=sys.stderr)
1150+
print(f"=== Total: {len(sorted_by_name)} fragments ===\n", file=sys.stderr)
1151+
11021152
def __build_namespace_element_lists(self):
11031153
"""
11041154
This function builds the lists needed to generate the root struct and root decoding function.

src/cbexigen/base_coder_classes.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,10 @@ def append_to_element_grammars(self, grammar: ElementGrammar, element_typename):
425425

426426
def generate_element_grammars(self, element: ElementData):
427427
self.reset_element_grammars()
428+
# NOTE: Attribute particles are pre-sorted by SchemaAnalyzer.
429+
# When canonical_exi_enabled=1, attributes are sorted by (local_name, namespace_uri)
430+
# with xsi:type first and xsi:nil second, per W3C Canonical EXI spec.
431+
# Child element particles remain in schema-defined order.
428432
particle_is_part_of_sequence = False
429433

430434
# if the current element type is in the namespace elements dict,

src/cbexigen/elementData.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ class Particle:
5656
integer_is_unsigned: bool = False
5757
# additional info for the anyType particle
5858
process_content: str = None
59+
# namespace URI for canonical EXI attribute ordering
60+
namespace_uri: str = ''
5961

6062
@property
6163
def max_occurs_old(self):

src/cbexigen/tools_config.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
CONFIG_PARAMS: Dict[str, Union[str, int]] = {
2323
# add debug code while generating code
2424
'add_debug_code': 0,
25+
# enable canonical EXI code generation
26+
'canonical_exi_enabled': 0,
2527
# generate analysis tree while generating code
2628
'generate_analysis_tree': 0,
2729
'generate_analysis_tree_20': 0,
@@ -113,6 +115,11 @@ def process_config_parameters():
113115
if hasattr(config_module, 'add_debug_code'):
114116
CONFIG_PARAMS['add_debug_code'] = config_module.add_debug_code
115117

118+
''' canonical EXI definitions '''
119+
# canonical_exi_enabled
120+
if hasattr(config_module, 'canonical_exi_enabled'):
121+
CONFIG_PARAMS['canonical_exi_enabled'] = config_module.canonical_exi_enabled
122+
116123
''' analysis tree definitions '''
117124
# generate_analysis_tree
118125
if hasattr(config_module, 'generate_analysis_tree'):

src/config.py

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@
2020
# and create separate code for the debugging functions
2121
add_debug_code = 0
2222

23+
# enable canonical EXI code generation
24+
# this will add a canonical_mode field to exi_bitstream_t
25+
# and enable Canonical EXI for Plug & Charge code paths in generated output
26+
# mode=0 (EXI_MODE_STANDARD): schema-informed standard EXI (0x80 header, bit-packed)
27+
# mode=1 (EXI_MODE_CANONICAL): Canonical EXI for Plug & Charge (0x80 header, bit-packed, type-aware)
28+
canonical_exi_enabled = 0
29+
2330
# generate analysis tree while generating code
2431
# this will generate an analysis tree file starting from the root element
2532
# for the 15118-20 every message has its separate tree file
@@ -438,7 +445,8 @@
438445
'filename': 'iso2_msgDefEncoder.c',
439446
'identifier': 'ISO2_MSG_DEF_ENCODER_C',
440447
'include_std_lib': ['stdint.h'],
441-
'include_other': ['exi_basetypes.h', 'exi_basetypes_encoder.h', 'exi_error_codes.h', 'exi_header.h',
448+
'include_other': ['exi_basetypes.h', 'exi_basetypes_encoder.h', 'exi_types_encoder.h',
449+
'exi_error_codes.h', 'exi_header.h',
442450
'iso2_msgDefDatatypes.h', 'iso2_msgDefEncoder.h']
443451
}
444452
},
@@ -495,7 +503,8 @@
495503
'filename': 'iso20_CommonMessages_Encoder.c',
496504
'identifier': 'ISO20_COMMON_MESSAGES_ENCODER_C',
497505
'include_std_lib': ['stdint.h'],
498-
'include_other': ['exi_basetypes.h', 'exi_basetypes_encoder.h', 'exi_error_codes.h', 'exi_header.h',
506+
'include_other': ['exi_basetypes.h', 'exi_basetypes_encoder.h', 'exi_types_encoder.h',
507+
'exi_error_codes.h', 'exi_header.h',
499508
'iso20_CommonMessages_Datatypes.h', 'iso20_CommonMessages_Encoder.h']
500509
}
501510
},
@@ -551,7 +560,8 @@
551560
'filename': 'iso20_AC_Encoder.c',
552561
'identifier': 'ISO20_AC_ENCODER_C',
553562
'include_std_lib': ['stdint.h'],
554-
'include_other': ['exi_basetypes.h', 'exi_basetypes_encoder.h', 'exi_error_codes.h', 'exi_header.h',
563+
'include_other': ['exi_basetypes.h', 'exi_basetypes_encoder.h', 'exi_types_encoder.h',
564+
'exi_error_codes.h', 'exi_header.h',
555565
'iso20_AC_Datatypes.h', 'iso20_AC_Encoder.h']
556566
}
557567
},
@@ -608,7 +618,8 @@
608618
'filename': 'iso20_DC_Encoder.c',
609619
'identifier': 'ISO20_DC_ENCODER_C',
610620
'include_std_lib': ['stdint.h'],
611-
'include_other': ['exi_basetypes.h', 'exi_basetypes_encoder.h', 'exi_error_codes.h', 'exi_header.h',
621+
'include_other': ['exi_basetypes.h', 'exi_basetypes_encoder.h', 'exi_types_encoder.h',
622+
'exi_error_codes.h', 'exi_header.h',
612623
'iso20_DC_Datatypes.h', 'iso20_DC_Encoder.h']
613624
}
614625
},
@@ -665,7 +676,8 @@
665676
'filename': 'iso20_WPT_Encoder.c',
666677
'identifier': 'ISO20_WPT_ENCODER_C',
667678
'include_std_lib': ['stdint.h'],
668-
'include_other': ['exi_basetypes.h', 'exi_basetypes_encoder.h', 'exi_error_codes.h', 'exi_header.h',
679+
'include_other': ['exi_basetypes.h', 'exi_basetypes_encoder.h', 'exi_types_encoder.h',
680+
'exi_error_codes.h', 'exi_header.h',
669681
'iso20_WPT_Datatypes.h', 'iso20_WPT_Encoder.h']
670682
}
671683
},
@@ -722,7 +734,8 @@
722734
'filename': 'iso20_ACDP_Encoder.c',
723735
'identifier': 'ISO20_ACDP_ENCODER_C',
724736
'include_std_lib': ['stdint.h'],
725-
'include_other': ['exi_basetypes.h', 'exi_basetypes_encoder.h', 'exi_error_codes.h', 'exi_header.h',
737+
'include_other': ['exi_basetypes.h', 'exi_basetypes_encoder.h', 'exi_types_encoder.h',
738+
'exi_error_codes.h', 'exi_header.h',
726739
'iso20_ACDP_Datatypes.h', 'iso20_ACDP_Encoder.h']
727740
}
728741
},
Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
{{ indent * level }}{{ decode_comment }}
2-
{{ indent * level }}error = exi_basetypes_decoder_uint_16(stream, &{{ type_content_len }});
3-
{{ indent * level }}if (error == 0)
4-
{{ indent * level }}{
5-
{{ indent * (level + 1) }}error = exi_basetypes_decoder_bytes(stream, {{ type_content_len }}, &{{ type_content }}[0], {{ type_define }});
2+
{{ indent * (level + 1) }}error = exi_basetypes_decoder_uint_16(stream, &{{ type_content_len }});
63
{{ indent * (level + 1) }}if (error == 0)
74
{{ indent * (level + 1) }}{
5+
{{ indent * (level + 2) }}error = exi_basetypes_decoder_bytes(stream, {{ type_content_len }}, &{{ type_content }}[0], {{ type_define }});
6+
{{ indent * (level + 2) }}if (error == 0)
7+
{{ indent * (level + 2) }}{
88
{%- if type_option == 1 %}
9-
{{ indent * (level + 2) }}{{ type_value }}_isUsed = 1u;
9+
{{ indent * (level + 3) }}{{ type_value }}_isUsed = 1u;
1010
{%- endif %}
11-
{{ indent * (level + 2) }}grammar_id = {{ next_grammar_id }};
11+
{{ indent * (level + 3) }}grammar_id = {{ next_grammar_id }};
12+
{{ indent * (level + 2) }}}
1213
{{ indent * (level + 1) }}}
13-
{{ indent * level }}}

src/input/code_templates/c/static_code/exi_basetypes_encoder.c.jinja

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,8 @@ int exi_basetypes_encoder_bool(exi_bitstream_t* stream, int value)
4848
stream->status_callback(EXI_DEBUG__BASETYPES_ENCODE_BOOL, 0, (int)value, 0);
4949
}
5050
{% endif %}
51-
int error;
5251
uint32_t bit = (value) ? 1 : 0;
53-
54-
error = exi_bitstream_write_bits(stream, 1, bit);
55-
56-
return error;
52+
return exi_bitstream_write_bits(stream, 1, bit);
5753
}
5854

5955
/*****************************************************************************
@@ -304,6 +300,18 @@ int exi_basetypes_encoder_signed(exi_bitstream_t* stream, const exi_signed_t* va
304300
return exi_basetypes_encoder_unsigned(stream, &value->data);
305301
}
306302

303+
/*****************************************************************************
304+
* interface functions - string length encoding
305+
*****************************************************************************/
306+
{%- if canonical_exi_enabled == 1 %}
307+
int exi_basetypes_encoder_string_len(exi_bitstream_t* stream, uint16_t characters_len)
308+
{
309+
/* Standard EXI string table miss encoding: length + 2 offset.
310+
* Used by both EXI_MODE_STANDARD and EXI_MODE_CANONICAL (Plug & Charge profile). */
311+
return exi_basetypes_encoder_uint_16(stream, (uint16_t)(characters_len + 2));
312+
}
313+
{%- endif %}
314+
307315
/*****************************************************************************
308316
* interface functions - characters, string
309317
*****************************************************************************/

src/input/code_templates/c/static_code/exi_basetypes_encoder.h.jinja

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,4 +95,18 @@ int exi_basetypes_encoder_signed(exi_bitstream_t* stream, const exi_signed_t* va
9595
*
9696
*/
9797
int exi_basetypes_encoder_characters(exi_bitstream_t* stream, size_t characters_len, const exi_character_t* characters, size_t characters_size);
98+
99+
/**
100+
* \brief encoder for string length with canonical mode support
101+
*
102+
* In canonical mode (Preserve.lexicalValues=true), writes string table
103+
* partition prefix before the string length per W3C EXI Section 7.1.3.
104+
* In non-canonical mode, writes just the string length.
105+
*
106+
* \param stream EXI bitstream
107+
* \param characters_len length of the string
108+
* \return NO_ERROR or error code
109+
*
110+
*/
111+
int exi_basetypes_encoder_string_len(exi_bitstream_t* stream, uint16_t characters_len);
98112
{% endblock %}

src/input/code_templates/c/static_code/exi_bitstream.c.jinja

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,9 @@ void exi_bitstream_init(exi_bitstream_t* stream, uint8_t* data, size_t data_size
106106
stream->_flag_byte_pos = data_offset;
107107

108108
stream->status_callback = status_callback;
109+
{%- if canonical_exi_enabled == 1 %}
110+
stream->canonical_mode = 0;
111+
{%- endif %}
109112
{%- if add_debug_code == 1 %}
110113

111114
if (stream->status_callback)

0 commit comments

Comments
 (0)