diff --git a/BUILD.gn b/BUILD.gn index 6cc0d55261ff2b..5b3a9e480b6336 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -54,6 +54,7 @@ if (current_toolchain != "${dir_pw_toolchain}/default:default") { "${chip_root}/src/credentials/tests:fuzz-chip-cert", "${chip_root}/src/lib/core/tests:fuzz-tlv-reader", "${chip_root}/src/lib/dnssd/minimal_mdns/tests:fuzz-minmdns-packet-parsing", + "${chip_root}/src/lib/format/tests:fuzz-payload-decoder", ] } } diff --git a/build/chip/chip_codegen.gni b/build/chip/chip_codegen.gni index 64c0642da29e65..1a6927bf4686f4 100644 --- a/build/chip/chip_codegen.gni +++ b/build/chip/chip_codegen.gni @@ -53,9 +53,19 @@ template("_chip_build_time_codegen") { rebase_path(target_gen_dir, root_build_dir), "--expected-outputs", rebase_path(_expected_outputs, root_build_dir), - rebase_path(_idl_file, root_build_dir), ] + if (defined(invoker.options)) { + foreach(option, invoker.options) { + args += [ + "--option", + option, + ] + } + } + + args += [ rebase_path(_idl_file, root_build_dir) ] + inputs = [ _idl_file, _expected_outputs, @@ -313,12 +323,15 @@ template("chip_codegen") { "generator", "input", "outputs", + "options", "public_configs", ]) } } else { _name = target_name + not_needed(invoker, [ "options" ]) + # This constructs a path like: # FROM all-clusters-app.matter (inside examples/all-clusters-app/all-clusters-common/) # USING "cpp-app" for generator: diff --git a/examples/chip-tool/args.gni b/examples/chip-tool/args.gni index a6f6dcecb5a24f..96dd37f02f03a1 100644 --- a/examples/chip-tool/args.gni +++ b/examples/chip-tool/args.gni @@ -27,3 +27,6 @@ matter_enable_tracing_support = true # Perfetto requires C++17 cpp_standard = "gnu++17" + +matter_log_json_payload_hex = true +matter_log_json_payload_decode_full = true diff --git a/scripts/pregenerate/__init__.py b/scripts/pregenerate/__init__.py index 53c1e658176b19..a6f759bab67cf8 100644 --- a/scripts/pregenerate/__init__.py +++ b/scripts/pregenerate/__init__.py @@ -20,7 +20,8 @@ from typing import Iterator, List, Optional from .types import IdlFileType, InputIdlFile -from .using_codegen import CodegenCppAppPregenerator, CodegenJavaClassPregenerator, CodegenJavaJNIPregenerator +from .using_codegen import (CodegenCppAppPregenerator, CodegenCppClustersTLVMetaPregenerator, + CodegenCppProtocolsTLVMetaPregenerator, CodegenJavaClassPregenerator, CodegenJavaJNIPregenerator) from .using_zap import ZapApplicationPregenerator @@ -94,6 +95,8 @@ def FindPregenerationTargets(sdk_root: str, external_roots: Optional[List[str]], CodegenJavaJNIPregenerator(sdk_root), CodegenJavaClassPregenerator(sdk_root), CodegenCppAppPregenerator(sdk_root), + CodegenCppClustersTLVMetaPregenerator(sdk_root), + CodegenCppProtocolsTLVMetaPregenerator(sdk_root), # ZAP codegen ZapApplicationPregenerator(sdk_root), diff --git a/scripts/pregenerate/using_codegen.py b/scripts/pregenerate/using_codegen.py index 5d790d808d40b6..a48f187f5d9466 100644 --- a/scripts/pregenerate/using_codegen.py +++ b/scripts/pregenerate/using_codegen.py @@ -27,11 +27,12 @@ class CodegenTarget: """A target that uses `scripts/codegen.py` to generate files.""" - def __init__(self, idl: InputIdlFile, generator: str, sdk_root: str, runner): + def __init__(self, idl: InputIdlFile, generator: str, sdk_root: str, runner, options=[]): self.idl = idl self.generator = generator self.sdk_root = sdk_root self.runner = runner + self.options = options if idl.file_type != IdlFileType.MATTER: raise Exception( @@ -51,8 +52,12 @@ def Generate(self, output_root: str): '--log-level', 'fatal', '--generator', self.generator, '--output-dir', output_dir, - self.idl.full_path ] + for option in self.options: + cmd.append("--option") + cmd.append(option) + + cmd.append(self.idl.full_path) logging.debug(f"Executing {cmd}") self.runner.run(cmd) @@ -97,6 +102,9 @@ def Accept(self, idl: InputIdlFile): if idl.file_type != IdlFileType.MATTER: return False + if '/lib/format/' in idl.relative_path: + return False + # we should not be checked for these, but verify just in case if '/tests/' in idl.relative_path: return False @@ -105,3 +113,29 @@ def Accept(self, idl: InputIdlFile): def CreateTarget(self, idl: InputIdlFile, runner): return CodegenTarget(sdk_root=self.sdk_root, idl=idl, generator="cpp-app", runner=runner) + + +class CodegenCppProtocolsTLVMetaPregenerator: + """Pregeneration logic for "cpp-app" codegen.py outputs""" + + def __init__(self, sdk_root): + self.sdk_root = sdk_root + + def Accept(self, idl: InputIdlFile): + return (idl.file_type == IdlFileType.MATTER) and idl.relative_path.endswith('/protocol_messages.matter') + + def CreateTarget(self, idl: InputIdlFile, runner): + return CodegenTarget(sdk_root=self.sdk_root, idl=idl, generator="cpp-tlvmeta", options=["table_name:protocols_meta"], runner=runner) + + +class CodegenCppClustersTLVMetaPregenerator: + """Pregeneration logic for "cpp-app" codegen.py outputs""" + + def __init__(self, sdk_root): + self.sdk_root = sdk_root + + def Accept(self, idl: InputIdlFile): + return (idl.file_type == IdlFileType.MATTER) and idl.relative_path.endswith('/controller-clusters.matter') + + def CreateTarget(self, idl: InputIdlFile, runner): + return CodegenTarget(sdk_root=self.sdk_root, idl=idl, generator="cpp-tlvmeta", options=["table_name:clusters_meta"], runner=runner) diff --git a/scripts/py_matter_idl/files.gni b/scripts/py_matter_idl/files.gni index 0118c1aa7a3909..1f7a7b0271196d 100644 --- a/scripts/py_matter_idl/files.gni +++ b/scripts/py_matter_idl/files.gni @@ -3,12 +3,18 @@ import("//build_overrides/chip.gni") # Templates used for generation matter_idl_generator_templates = [ + "${chip_root}/scripts/py_matter_idl/matter_idl/generators/cpp/application/CallbackStubSource.jinja", + "${chip_root}/scripts/py_matter_idl/matter_idl/generators/cpp/application/PluginApplicationCallbacksHeader.jinja", + "${chip_root}/scripts/py_matter_idl/matter_idl/generators/cpp/tlvmeta/TLVMetaData_cpp.jinja", + "${chip_root}/scripts/py_matter_idl/matter_idl/generators/cpp/tlvmeta/TLVMetaData_h.jinja", + "${chip_root}/scripts/py_matter_idl/matter_idl/generators/java/CHIPCallbackTypes.jinja", "${chip_root}/scripts/py_matter_idl/matter_idl/generators/java/ChipClustersCpp.jinja", "${chip_root}/scripts/py_matter_idl/matter_idl/generators/java/ChipClustersRead.jinja", - "${chip_root}/scripts/py_matter_idl/matter_idl/generators/java/ClusterWriteMapping.jinja", + "${chip_root}/scripts/py_matter_idl/matter_idl/generators/java/CHIPGlobalCallbacks_cpp.jinja", + "${chip_root}/scripts/py_matter_idl/matter_idl/generators/java/CHIPReadCallbacks_h.jinja", "${chip_root}/scripts/py_matter_idl/matter_idl/generators/java/ClusterIDMapping.jinja", - "${chip_root}/scripts/py_matter_idl/matter_idl/generators/cpp/application/CallbackStubSource.jinja", - "${chip_root}/scripts/py_matter_idl/matter_idl/generators/cpp/application/PluginApplicationCallbacksHeader.jinja", + "${chip_root}/scripts/py_matter_idl/matter_idl/generators/java/ClusterReadMapping.jinja", + "${chip_root}/scripts/py_matter_idl/matter_idl/generators/java/ClusterWriteMapping.jinja", ] matter_idl_generator_sources = [ @@ -16,11 +22,19 @@ matter_idl_generator_sources = [ "${chip_root}/scripts/py_matter_idl/matter_idl/generators/__init__.py", "${chip_root}/scripts/py_matter_idl/matter_idl/generators/cpp/__init__.py", "${chip_root}/scripts/py_matter_idl/matter_idl/generators/cpp/application/__init__.py", + "${chip_root}/scripts/py_matter_idl/matter_idl/generators/cpp/tlvmeta/__init__.py", "${chip_root}/scripts/py_matter_idl/matter_idl/generators/filters.py", "${chip_root}/scripts/py_matter_idl/matter_idl/generators/java/__init__.py", + "${chip_root}/scripts/py_matter_idl/matter_idl/generators/registry.py", "${chip_root}/scripts/py_matter_idl/matter_idl/generators/types.py", + "${chip_root}/scripts/py_matter_idl/matter_idl/lint/__init__.py", + "${chip_root}/scripts/py_matter_idl/matter_idl/lint/lint_rules_parser.py", + "${chip_root}/scripts/py_matter_idl/matter_idl/lint/types.py", "${chip_root}/scripts/py_matter_idl/matter_idl/matter_idl_parser.py", "${chip_root}/scripts/py_matter_idl/matter_idl/matter_idl_types.py", + "${chip_root}/scripts/py_matter_idl/matter_idl/test_generators.py", + "${chip_root}/scripts/py_matter_idl/matter_idl/test_matter_idl_parser.py", + "${chip_root}/scripts/py_matter_idl/matter_idl/test_xml_parser.py", "${chip_root}/scripts/py_matter_idl/matter_idl/xml_parser.py", "${chip_root}/scripts/py_matter_idl/matter_idl/zapxml/__init__.py", "${chip_root}/scripts/py_matter_idl/matter_idl/zapxml/handlers/__init__.py", diff --git a/scripts/py_matter_idl/matter_idl/generators/cpp/tlvmeta/TLVMetaData_cpp.jinja b/scripts/py_matter_idl/matter_idl/generators/cpp/tlvmeta/TLVMetaData_cpp.jinja new file mode 100644 index 00000000000000..f748875b7d7996 --- /dev/null +++ b/scripts/py_matter_idl/matter_idl/generators/cpp/tlvmeta/TLVMetaData_cpp.jinja @@ -0,0 +1,44 @@ +#include + +namespace chip { +namespace TLVMeta { +namespace { + +using namespace chip::FlatTree; +using namespace chip::TLV; + +{%- for table in sub_tables %} + +const Entry _{{table.full_name}}[] = { + {%- for entry in table.entries %} + { { {{entry.code}}, "{{entry.name}}", ItemType::{{entry.item_type}} }, {{entry.reference | indexInTable(sub_tables)}} }, // {{entry.real_type}} + {%- endfor %} +}; +{%- endfor %} + +const Entry _all_clusters[] = { +{%- for cluster in clusters | sort(attribute='code') %} + { { ClusterTag({{"0x%02X" | format(cluster.code)}}), "{{cluster.name}}", ItemType::kDefault }, {{cluster.name | indexInTable(sub_tables)}} }, +{%- endfor %} + +}; + +// For any non-structure list like u64[] or similar. +const Entry _primitive_type_list[] = { + { { AnonymousTag(), "[]", ItemType::kDefault }, kInvalidNodeIndex }, +}; + +} // namespace + +#define _ENTRY(n) { sizeof(n) / sizeof(n[0]), n} + +const std::array, {{ sub_tables | length }} + 2> {{table_name}} = { { + _ENTRY(_all_clusters), // 0 + _ENTRY(_primitive_type_list), // 1 +{%- for table in sub_tables %} + _ENTRY(_{{table.full_name}}), // {{loop.index + 1}} +{%- endfor %} +} }; + +} // namespace TLVMeta +} // namespace chip diff --git a/scripts/py_matter_idl/matter_idl/generators/cpp/tlvmeta/TLVMetaData_h.jinja b/scripts/py_matter_idl/matter_idl/generators/cpp/tlvmeta/TLVMetaData_h.jinja new file mode 100644 index 00000000000000..f8d1e77b60de33 --- /dev/null +++ b/scripts/py_matter_idl/matter_idl/generators/cpp/tlvmeta/TLVMetaData_h.jinja @@ -0,0 +1,12 @@ +#include +#include + +#include + +namespace chip { +namespace TLVMeta { + +extern const std::array, {{ sub_tables | length }} + 2> {{table_name}}; + +} // namespace TLVMeta +} // namespace chip diff --git a/scripts/py_matter_idl/matter_idl/generators/cpp/tlvmeta/__init__.py b/scripts/py_matter_idl/matter_idl/generators/cpp/tlvmeta/__init__.py new file mode 100644 index 00000000000000..0bd57e28ac69f2 --- /dev/null +++ b/scripts/py_matter_idl/matter_idl/generators/cpp/tlvmeta/__init__.py @@ -0,0 +1,299 @@ +#!/usr/bin/env python +# Copyright (c) 2023 Project CHIP Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os +from dataclasses import dataclass +from typing import Generator, List, Optional + +from matter_idl.generators import CodeGenerator, GeneratorStorage +from matter_idl.matter_idl_types import Cluster, ClusterSide, Field, Idl, StructTag + + +@dataclass +class TableEntry: + code: str # Encoding like ContextTag() or AnonymousTag() or similar + name: str # human friendly name + reference: Optional[str] # reference to full name + real_type: str # real type + item_type: str = 'kDefault' # type flag for decoding + + +@dataclass +class Table: + # Usable variable fully qualified name (like _) + full_name: str + entries: List[TableEntry] + + +class ClusterTablesGenerator: + """Handles conversion from a cluster to tables.""" + + def __init__(self, cluster: Cluster): + self.cluster = cluster + self.known_types = set() # all types where we create reference_to + self.list_types = set() # all types that require a list entry + self.item_type_map = { + "protocol_cluster_id": "kProtocolClusterId", + "protocol_attribute_id": "kProtocolAttributeId", + "protocol_command_id": "kProtocolCommandId", + "protocol_event_id": "kProtocolEventId", + + "cluster_attribute_payload": "kProtocolPayloadAttribute", + "cluster_command_payload": "kProtocolPayloadCommand", + "cluster_event_payload": "kProtocolPayloadEvent", + + "protocol_binary_data": "kProtocolBinaryData", + } + + for e in self.cluster.enums: + self.item_type_map[e.name] = "kEnum" + + for b in self.cluster.bitmaps: + self.item_type_map[b.name] = "kBitmap" + + def FieldEntry(self, field: Field, tag_type: str = 'ContextTag') -> TableEntry: + type_reference = "%s_%s" % (self.cluster.name, field.data_type.name) + if type_reference not in self.known_types: + type_reference = None + + item_type = self.item_type_map.get(field.data_type.name, 'kDefault') + + real_type = "%s::%s" % (self.cluster.name, field.data_type.name) + if field.is_list: + real_type = real_type + "[]" + item_type = "kList" + + if type_reference: + self.list_types.add(type_reference) + type_reference = type_reference + "_list_" + else: + type_reference = "primitive_type_list_" + + return TableEntry( + code=f'{tag_type}({field.code})', + name=field.name, + reference=type_reference, + real_type=real_type, + item_type=item_type, + ) + + def ComputeKnownTypes(self): + self.known_types.clear() + + for s in self.cluster.structs: + self.known_types.add("%s_%s" % (self.cluster.name, s.name)) + + # Events are structures + for e in self.cluster.events: + self.known_types.add("%s_%s" % (self.cluster.name, e.name)) + + for e in self.cluster.enums: + self.known_types.add("%s_%s" % (self.cluster.name, e.name)) + + for b in self.cluster.bitmaps: + self.known_types.add("%s_%s" % (self.cluster.name, b.name)) + + def CommandEntries(self) -> Generator[TableEntry, None, None]: + # yield entries for every command input + for c in self.cluster.commands: + if c.input_param: + yield TableEntry( + name=c.name, + code=f'CommandTag({c.code})', + reference="%s_%s" % ( + self.cluster.name, c.input_param), + real_type="%s::%s::%s" % ( + self.cluster.name, c.name, c.input_param) + ) + else: + yield TableEntry( + name=c.name, + code=f'CommandTag({c.code})', + reference=None, + real_type="%s::%s::()" % ( + self.cluster.name, c.name) + ) + + # yield entries for every command output. We use "respons struct" + # for this to figure out where to tag IDs from. + for c in self.cluster.structs: + if c.tag != StructTag.RESPONSE: + continue + yield TableEntry( + name=c.name, + code=f'CommandTag({c.code})', + reference="%s_%s" % ( + self.cluster.name, c.name), + real_type="%s::%s" % (self.cluster.name, c.name), + ) + + def GenerateTables(self) -> Generator[Table, None, None]: + self.ComputeKnownTypes() + + # Clusters have attributes. They are direct descendants for + # attributes + cluster_entries = [] + cluster_entries.extend([self.FieldEntry( + a.definition, tag_type='AttributeTag') for a in self.cluster.attributes]) + + cluster_entries.extend([ + # events always reference an existing struct + TableEntry( + code=f'EventTag({e.code})', + name=e.name, + reference="%s_%s" % (self.cluster.name, e.name), + real_type='%s::%s' % (self.cluster.name, e.name) + ) + for e in self.cluster.events + ]) + cluster_entries.extend( + [entry for entry in self.CommandEntries()] + ) + + yield Table( + full_name=self.cluster.name, + entries=cluster_entries, + ) + + for s in self.cluster.structs: + yield Table( + full_name="%s_%s" % (self.cluster.name, s.name), + entries=[self.FieldEntry(field) for field in s.fields] + ) + + for e in self.cluster.events: + yield Table( + full_name="%s_%s" % (self.cluster.name, e.name), + entries=[self.FieldEntry(field) for field in e.fields] + ) + + # some items have lists, create an intermediate item for those + for name in self.list_types: + yield Table( + full_name="%s_list_" % name, + entries=[ + TableEntry( + code="AnonymousTag()", + name="[]", + reference=name, + real_type="%s[]" % name, + ) + ] + ) + + for e in self.cluster.enums: + yield Table( + full_name="%s_%s" % (self.cluster.name, e.name), + entries=[ + TableEntry( + code="ConstantValueTag(0x%X)" % entry.code, + name=entry.name, + reference=None, + real_type="%s::%s::%s" % (self.cluster.name, e.name, entry.name) + ) + for entry in e.entries + ] + ) + + for e in self.cluster.bitmaps: + yield Table( + full_name="%s_%s" % (self.cluster.name, e.name), + entries=[ + TableEntry( + code="ConstantValueTag(0x%X)" % entry.code, + name=entry.name, + reference=None, + real_type="%s::%s::%s" % (self.cluster.name, e.name, entry.name) + ) + for entry in e.entries + ] + ) + + +def CreateTables(idl: Idl) -> List[Table]: + result = [] + for cluster in idl.clusters: + result.extend( + [table for table in ClusterTablesGenerator(cluster).GenerateTables()]) + + return result + + +def IndexInTable(name: Optional[str], table: List[Table]) -> str: + """Find the index of the given name in the table. + + The index is 1-based (to allow for a first entry containing a + starting point for the app) + """ + if not name: + return "kInvalidNodeIndex" + + if name == "primitive_type_list_": + return "1" + + for idx, t in enumerate(table): + if t.full_name == name: + # Index skipping hard-coded items + return idx + 2 + + raise Exception("Name %r not found in table" % name) + + +class TLVMetaDataGenerator(CodeGenerator): + """ + Generation of cpp code containing TLV metadata information. + + Epecting extra option for constant naming + + Example execution via codegen.py: + + ./scripts/codegen.py \ + --output-dir out/metaexample \ + --generator cpp-tlvmeta \ + --option table_name:protocols_meta \ + src/lib/format/protocol_messages.matter + """ + + def __init__(self, storage: GeneratorStorage, idl: Idl, table_name: str = "clusters_meta", **kargs): + super().__init__(storage, idl, fs_loader_searchpath=os.path.dirname(__file__)) + self.table_name = table_name + self.jinja_env.filters['indexInTable'] = IndexInTable + + def internal_render_all(self): + """ + Renders the cpp and header files required for applications + """ + + tables = CreateTables(self.idl) + + self.internal_render_one_output( + template_path="TLVMetaData_cpp.jinja", + output_file_name=f"tlv/meta/{self.table_name}.cpp", + vars={ + 'clusters': [c for c in self.idl.clusters if c.side == ClusterSide.CLIENT], + 'table_name': self.table_name, + 'sub_tables': tables, + + } + ) + + self.internal_render_one_output( + template_path="TLVMetaData_h.jinja", + output_file_name=f"tlv/meta/{self.table_name}.h", + vars={ + 'clusters': [c for c in self.idl.clusters if c.side == ClusterSide.CLIENT], + 'table_name': self.table_name, + 'sub_tables': tables, + } + ) diff --git a/scripts/py_matter_idl/matter_idl/generators/registry.py b/scripts/py_matter_idl/matter_idl/generators/registry.py index 7f1e17bc4d05a7..e3c8e1ed32397e 100644 --- a/scripts/py_matter_idl/matter_idl/generators/registry.py +++ b/scripts/py_matter_idl/matter_idl/generators/registry.py @@ -16,6 +16,7 @@ import importlib from matter_idl.generators.cpp.application import CppApplicationGenerator +from matter_idl.generators.cpp.tlvmeta import TLVMetaDataGenerator from matter_idl.generators.java import JavaClassGenerator, JavaJNIGenerator @@ -28,6 +29,7 @@ class CodeGenerator(enum.Enum): JAVA_JNI = enum.auto() JAVA_CLASS = enum.auto() CPP_APPLICATION = enum.auto() + CPP_TLVMETA = enum.auto() CUSTOM = enum.auto() def Create(self, *args, **kargs): @@ -37,6 +39,8 @@ def Create(self, *args, **kargs): return JavaClassGenerator(*args, **kargs) elif self == CodeGenerator.CPP_APPLICATION: return CppApplicationGenerator(*args, **kargs) + elif self == CodeGenerator.CPP_TLVMETA: + return TLVMetaDataGenerator(*args, **kargs) elif self == CodeGenerator.CUSTOM: # Use a package naming convention to find the custom generator: # ./matter_idl_plugin/__init__.py defines a subclass of CodeGenerator named CustomGenerator. @@ -65,5 +69,6 @@ def FromString(name): 'java-jni': CodeGenerator.JAVA_JNI, 'java-class': CodeGenerator.JAVA_CLASS, 'cpp-app': CodeGenerator.CPP_APPLICATION, + 'cpp-tlvmeta': CodeGenerator.CPP_TLVMETA, 'custom': CodeGenerator.CUSTOM, } diff --git a/scripts/py_matter_idl/matter_idl/test_generators.py b/scripts/py_matter_idl/matter_idl/test_generators.py index 146b6244de6cb2..0576f3222e8bd5 100755 --- a/scripts/py_matter_idl/matter_idl/test_generators.py +++ b/scripts/py_matter_idl/matter_idl/test_generators.py @@ -32,6 +32,7 @@ from matter_idl.generators import GeneratorStorage from matter_idl.generators.cpp.application import CppApplicationGenerator +from matter_idl.generators.cpp.tlvmeta import TLVMetaDataGenerator from matter_idl.generators.java import JavaClassGenerator, JavaJNIGenerator from matter_idl.matter_idl_types import Idl @@ -121,6 +122,8 @@ def _create_generator(self, storage: GeneratorStorage, idl: Idl): return JavaClassGenerator(storage, idl) if self.generator_name.lower() == 'cpp-app': return CppApplicationGenerator(storage, idl) + if self.generator_name.lower() == 'cpp-tlvmeta': + return TLVMetaDataGenerator(storage, idl, table_name="clusters_meta") if self.generator_name.lower() == 'custom-example-proto': sys.path.append(os.path.abspath( os.path.join(os.path.dirname(__file__), '../examples'))) diff --git a/scripts/py_matter_idl/matter_idl/tests/available_tests.yaml b/scripts/py_matter_idl/matter_idl/tests/available_tests.yaml index f8f757bef4b3e3..3a50ebc076dcb8 100644 --- a/scripts/py_matter_idl/matter_idl/tests/available_tests.yaml +++ b/scripts/py_matter_idl/matter_idl/tests/available_tests.yaml @@ -69,6 +69,14 @@ cpp-app: app/PluginApplicationCallbacks.h: outputs/large_lighting_app/cpp-app/PluginApplicationCallbacks.h app/callback-stub.cpp: outputs/large_lighting_app/cpp-app/callback-stub.cpp +cpp-tlvmeta: + inputs/cluster_with_commands.matter: + tlv/meta/clusters_meta.cpp: outputs/cluster_with_commands/cpp-tlvmeta/clusters_meta.cpp + tlv/meta/clusters_meta.h: outputs/cluster_with_commands/cpp-tlvmeta/clusters_meta.h + inputs/cluster_struct_attribute.matter: + tlv/meta/clusters_meta.cpp: outputs/cluster_struct_attribute/cpp-tlvmeta/clusters_meta.cpp + tlv/meta/clusters_meta.h: outputs/cluster_struct_attribute/cpp-tlvmeta/clusters_meta.h + custom-example-proto: inputs/several_clusters.matter: proto/first_cluster.proto: outputs/proto/first_cluster.proto diff --git a/scripts/py_matter_idl/matter_idl/tests/outputs/cluster_struct_attribute/cpp-tlvmeta/clusters_meta.cpp b/scripts/py_matter_idl/matter_idl/tests/outputs/cluster_struct_attribute/cpp-tlvmeta/clusters_meta.cpp new file mode 100644 index 00000000000000..4985c3e09a7dfa --- /dev/null +++ b/scripts/py_matter_idl/matter_idl/tests/outputs/cluster_struct_attribute/cpp-tlvmeta/clusters_meta.cpp @@ -0,0 +1,48 @@ +#include + +namespace chip { +namespace TLVMeta { +namespace { + +using namespace chip::FlatTree; +using namespace chip::TLV; + +const Entry _DemoCluster[] = { + { { AttributeTag(5), "singleFailSafe", ItemType::kDefault }, 3 }, // DemoCluster::ArmFailSafeRequest + { { AttributeTag(100), "armFailsafes", ItemType::kList }, 4 }, // DemoCluster::ArmFailSafeRequest[] +}; + +const Entry _DemoCluster_ArmFailSafeRequest[] = { + { { ContextTag(0), "expiryLengthSeconds", ItemType::kDefault }, kInvalidNodeIndex }, // DemoCluster::INT16U + { { ContextTag(1), "breadcrumb", ItemType::kDefault }, kInvalidNodeIndex }, // DemoCluster::INT64U + { { ContextTag(2), "timeoutMs", ItemType::kDefault }, kInvalidNodeIndex }, // DemoCluster::INT32U +}; + +const Entry _DemoCluster_ArmFailSafeRequest_list_[] = { + { { AnonymousTag(), "[]", ItemType::kDefault }, 3 }, // DemoCluster_ArmFailSafeRequest[] +}; + +const Entry _all_clusters[] = { + { { ClusterTag(0x0A), "DemoCluster", ItemType::kDefault }, 2 }, + +}; + +// For any non-structure list like u64[] or similar. +const Entry _primitive_type_list[] = { + { { AnonymousTag(), "[]", ItemType::kDefault }, kInvalidNodeIndex }, +}; + +} // namespace + +#define _ENTRY(n) { sizeof(n) / sizeof(n[0]), n} + +const std::array, 3 + 2> clusters_meta = { { + _ENTRY(_all_clusters), // 0 + _ENTRY(_primitive_type_list), // 1 + _ENTRY(_DemoCluster), // 2 + _ENTRY(_DemoCluster_ArmFailSafeRequest), // 3 + _ENTRY(_DemoCluster_ArmFailSafeRequest_list_), // 4 +} }; + +} // namespace TLVMeta +} // namespace chip diff --git a/scripts/py_matter_idl/matter_idl/tests/outputs/cluster_struct_attribute/cpp-tlvmeta/clusters_meta.h b/scripts/py_matter_idl/matter_idl/tests/outputs/cluster_struct_attribute/cpp-tlvmeta/clusters_meta.h new file mode 100644 index 00000000000000..c8a3aa64885266 --- /dev/null +++ b/scripts/py_matter_idl/matter_idl/tests/outputs/cluster_struct_attribute/cpp-tlvmeta/clusters_meta.h @@ -0,0 +1,12 @@ +#include +#include + +#include + +namespace chip { +namespace TLVMeta { + +extern const std::array, 3 + 2> clusters_meta; + +} // namespace TLVMeta +} // namespace chip diff --git a/scripts/py_matter_idl/matter_idl/tests/outputs/cluster_with_commands/cpp-tlvmeta/clusters_meta.cpp b/scripts/py_matter_idl/matter_idl/tests/outputs/cluster_with_commands/cpp-tlvmeta/clusters_meta.cpp new file mode 100644 index 00000000000000..05d308526e1368 --- /dev/null +++ b/scripts/py_matter_idl/matter_idl/tests/outputs/cluster_with_commands/cpp-tlvmeta/clusters_meta.cpp @@ -0,0 +1,79 @@ +#include + +namespace chip { +namespace TLVMeta { +namespace { + +using namespace chip::FlatTree; +using namespace chip::TLV; + +const Entry _OnOff[] = { + { { AttributeTag(0), "onOff", ItemType::kDefault }, kInvalidNodeIndex }, // OnOff::boolean + { { AttributeTag(65532), "featureMap", ItemType::kDefault }, kInvalidNodeIndex }, // OnOff::bitmap32 + { { AttributeTag(65533), "clusterRevision", ItemType::kDefault }, kInvalidNodeIndex }, // OnOff::int16u + { { CommandTag(0), "Off", ItemType::kDefault }, kInvalidNodeIndex }, // OnOff::Off::() + { { CommandTag(1), "On", ItemType::kDefault }, kInvalidNodeIndex }, // OnOff::On::() + { { CommandTag(2), "Toggle", ItemType::kDefault }, kInvalidNodeIndex }, // OnOff::Toggle::() +}; + +const Entry _OnOff_OnOffDelayedAllOffEffectVariant[] = { + { { ConstantValueTag(0x0), "kFadeToOffIn0p8Seconds", ItemType::kDefault }, kInvalidNodeIndex }, // OnOff::OnOffDelayedAllOffEffectVariant::kFadeToOffIn0p8Seconds + { { ConstantValueTag(0x1), "kNoFade", ItemType::kDefault }, kInvalidNodeIndex }, // OnOff::OnOffDelayedAllOffEffectVariant::kNoFade + { { ConstantValueTag(0x2), "k50PercentDimDownIn0p8SecondsThenFadeToOffIn12Seconds", ItemType::kDefault }, kInvalidNodeIndex }, // OnOff::OnOffDelayedAllOffEffectVariant::k50PercentDimDownIn0p8SecondsThenFadeToOffIn12Seconds +}; + +const Entry _OnOff_OnOffDyingLightEffectVariant[] = { + { { ConstantValueTag(0x0), "k20PercenterDimUpIn0p5SecondsThenFadeToOffIn1Second", ItemType::kDefault }, kInvalidNodeIndex }, // OnOff::OnOffDyingLightEffectVariant::k20PercenterDimUpIn0p5SecondsThenFadeToOffIn1Second +}; + +const Entry _OnOff_OnOffEffectIdentifier[] = { + { { ConstantValueTag(0x0), "kDelayedAllOff", ItemType::kDefault }, kInvalidNodeIndex }, // OnOff::OnOffEffectIdentifier::kDelayedAllOff + { { ConstantValueTag(0x1), "kDyingLight", ItemType::kDefault }, kInvalidNodeIndex }, // OnOff::OnOffEffectIdentifier::kDyingLight +}; + +const Entry _OnOff_OnOffStartUpOnOff[] = { + { { ConstantValueTag(0x0), "kOff", ItemType::kDefault }, kInvalidNodeIndex }, // OnOff::OnOffStartUpOnOff::kOff + { { ConstantValueTag(0x1), "kOn", ItemType::kDefault }, kInvalidNodeIndex }, // OnOff::OnOffStartUpOnOff::kOn + { { ConstantValueTag(0x2), "kTogglePreviousOnOff", ItemType::kDefault }, kInvalidNodeIndex }, // OnOff::OnOffStartUpOnOff::kTogglePreviousOnOff +}; + +const Entry _OnOff_OnOffControl[] = { + { { ConstantValueTag(0x1), "kAcceptOnlyWhenOn", ItemType::kDefault }, kInvalidNodeIndex }, // OnOff::OnOffControl::kAcceptOnlyWhenOn +}; + +const Entry _OnOff_OnOffFeature[] = { + { { ConstantValueTag(0x1), "kLighting", ItemType::kDefault }, kInvalidNodeIndex }, // OnOff::OnOffFeature::kLighting +}; + +const Entry _OnOff_ScenesFeature[] = { + { { ConstantValueTag(0x1), "kSceneNames", ItemType::kDefault }, kInvalidNodeIndex }, // OnOff::ScenesFeature::kSceneNames +}; + +const Entry _all_clusters[] = { + +}; + +// For any non-structure list like u64[] or similar. +const Entry _primitive_type_list[] = { + { { AnonymousTag(), "[]", ItemType::kDefault }, kInvalidNodeIndex }, +}; + +} // namespace + +#define _ENTRY(n) { sizeof(n) / sizeof(n[0]), n} + +const std::array, 8 + 2> clusters_meta = { { + _ENTRY(_all_clusters), // 0 + _ENTRY(_primitive_type_list), // 1 + _ENTRY(_OnOff), // 2 + _ENTRY(_OnOff_OnOffDelayedAllOffEffectVariant), // 3 + _ENTRY(_OnOff_OnOffDyingLightEffectVariant), // 4 + _ENTRY(_OnOff_OnOffEffectIdentifier), // 5 + _ENTRY(_OnOff_OnOffStartUpOnOff), // 6 + _ENTRY(_OnOff_OnOffControl), // 7 + _ENTRY(_OnOff_OnOffFeature), // 8 + _ENTRY(_OnOff_ScenesFeature), // 9 +} }; + +} // namespace TLVMeta +} // namespace chip diff --git a/scripts/py_matter_idl/matter_idl/tests/outputs/cluster_with_commands/cpp-tlvmeta/clusters_meta.h b/scripts/py_matter_idl/matter_idl/tests/outputs/cluster_with_commands/cpp-tlvmeta/clusters_meta.h new file mode 100644 index 00000000000000..f2bd0832877be6 --- /dev/null +++ b/scripts/py_matter_idl/matter_idl/tests/outputs/cluster_with_commands/cpp-tlvmeta/clusters_meta.h @@ -0,0 +1,12 @@ +#include +#include + +#include + +namespace chip { +namespace TLVMeta { + +extern const std::array, 8 + 2> clusters_meta; + +} // namespace TLVMeta +} // namespace chip diff --git a/scripts/py_matter_idl/setup.cfg b/scripts/py_matter_idl/setup.cfg index ae5b29a88e2947..2a7d386375a2e7 100644 --- a/scripts/py_matter_idl/setup.cfg +++ b/scripts/py_matter_idl/setup.cfg @@ -28,6 +28,8 @@ matter_idl = matter_grammar.lark generators/cpp/application/CallbackStubSource.jinja generators/cpp/application/PluginApplicationCallbacksHeader.jinja + generators/cpp/tlvmeta/TLVMetaData_cpp.jinja + generators/cpp/tlvmeta/TLVMetaData_h.jinja generators/java/CHIPCallbackTypes.jinja generators/java/ChipClustersCpp.jinja generators/java/ChipClustersRead.jinja diff --git a/src/BUILD.gn b/src/BUILD.gn index c36315ff4209a3..531ee984fee2c0 100644 --- a/src/BUILD.gn +++ b/src/BUILD.gn @@ -59,6 +59,7 @@ if (chip_build_tests) { "${chip_root}/src/lib/address_resolve/tests", "${chip_root}/src/lib/asn1/tests", "${chip_root}/src/lib/core/tests", + "${chip_root}/src/lib/format/tests", "${chip_root}/src/messaging/tests", "${chip_root}/src/protocols/bdx/tests", "${chip_root}/src/protocols/user_directed_commissioning/tests", diff --git a/src/controller/data_model/BUILD.gn b/src/controller/data_model/BUILD.gn index 72e981c8a60992..0bf3c15d80672f 100644 --- a/src/controller/data_model/BUILD.gn +++ b/src/controller/data_model/BUILD.gn @@ -21,6 +21,25 @@ import("${chip_root}/build/chip/chip_codegen.gni") import("${chip_root}/build/chip/java/config.gni") import("${chip_root}/src/app/chip_data_model.gni") +chip_codegen("cluster-tlv-metadata") { + input = "controller-clusters.matter" + generator = "cpp-tlvmeta" + + options = [ "table_name:clusters_meta" ] + + outputs = [ + "tlv/meta/clusters_meta.cpp", + "tlv/meta/clusters_meta.h", + ] + + deps = [ + "${chip_root}/src/lib/format:flat-tree", + "${chip_root}/src/lib/format:tlv-metadata-headers", + ] + + public_configs = [ "${chip_root}/src:includes" ] +} + chip_data_model("data_model") { zap_file = "controller-clusters.zap" diff --git a/src/lib/core/TLVTags.h b/src/lib/core/TLVTags.h index 435507637d9f71..fd2a8152b411f2 100644 --- a/src/lib/core/TLVTags.h +++ b/src/lib/core/TLVTags.h @@ -46,6 +46,8 @@ class Tag constexpr bool operator==(const Tag & other) const { return mVal == other.mVal; } constexpr bool operator!=(const Tag & other) const { return mVal != other.mVal; } + uint64_t RawValue() const { return mVal; } + private: explicit constexpr Tag(uint64_t val) : mVal(val) {} diff --git a/src/lib/format/BUILD.gn b/src/lib/format/BUILD.gn new file mode 100644 index 00000000000000..c75ecd084e1457 --- /dev/null +++ b/src/lib/format/BUILD.gn @@ -0,0 +1,70 @@ +# Copyright (c) 2023 Project CHIP Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import("//build_overrides/build.gni") +import("//build_overrides/chip.gni") + +import("${chip_root}/build/chip/chip_codegen.gni") + +source_set("flat-tree") { + sources = [ + "FlatTree.h", + "FlatTreePosition.h", + ] + + public_deps = [ "${chip_root}/src/lib/core" ] + + public_configs = [ "${chip_root}/src:includes" ] +} + +source_set("tlv-metadata-headers") { + sources = [ + "tlv_meta.h", # TODO: move in separate source set + ] +} + +chip_codegen("protocol-tlv-metadata") { + input = "protocol_messages.matter" + generator = "cpp-tlvmeta" + + options = [ "table_name:protocols_meta" ] + + outputs = [ + "tlv/meta/protocols_meta.cpp", + "tlv/meta/protocols_meta.h", + ] + + deps = [ + ":flat-tree", + ":tlv-metadata-headers", + ] + + public_configs = [ "${chip_root}/src:includes" ] +} + +source_set("protocol-decoder") { + sources = [ + "protocol_decoder.cpp", + "protocol_decoder.h", + ] + + public_deps = [ + ":flat-tree", + ":tlv-metadata-headers", + "${chip_root}/src/lib/core", + "${chip_root}/src/lib/support", + ] + + public_configs = [ "${chip_root}/src:includes" ] +} diff --git a/src/lib/format/FlatTree.h b/src/lib/format/FlatTree.h new file mode 100644 index 00000000000000..6c1eeb7dfd0802 --- /dev/null +++ b/src/lib/format/FlatTree.h @@ -0,0 +1,99 @@ +/* + * + * Copyright (c) 2023 Project CHIP Authors + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include + +#include +#include + +namespace chip { +namespace FlatTree { + +// A flat tree allows for tree data to be stored in a single flat +// array. + +/// Invalid indexes in a tree +static constexpr size_t kInvalidNodeIndex = std::numeric_limits::max(); + +/// An entry represents a single element identified by a key and containing a +/// value +/// +/// In a tree representation, every entry may potentially have a child node, +/// whose index is located in [node_index]. +template +struct Entry +{ + CONTENT data; + + // Node index is a valid index inside a node array if a entry has + // child elements, it is kInvalidNodeIndex otherwise; + size_t node_index; +}; + +template +struct Node +{ + size_t entry_count; // number of items in [entries] + const Entry * entries; // child items of [entry_count] size + + /// Attempt to find the entry with given matcher. + /// + /// Returns nullptr if no matches can be found. + template + const Entry * find_entry(MATCHER matcher) const + { + for (size_t i = 0; i < entry_count; i++) + { + if (matcher(entries[i].data)) + { + return &entries[i]; + } + } + return nullptr; + } +}; + +/// Search for a given entry in a sized array +/// +/// [data] is the flat tree array +/// [idx] is the index of the node to search. If out of bounds, nullptr is returned +/// [matcher] is the match function. +template +inline const Entry * FindEntry(const Node * content, size_t content_size, size_t idx, MATCHER matcher) +{ + if (idx >= content_size) + { + return nullptr; + } + return content[idx].find_entry(matcher); +} + +/// Search for a given entry in an array (array will do bounds check) +/// +/// [data] is the flat tree array +/// [idx] is the index of the node to search. If out of bounds, nullptr is returned +/// [matcher] is the match function. +template +inline const Entry * FindEntry(const std::array, N> & data, size_t idx, MATCHER matcher) +{ + return FindEntry(data.data(), N, idx, matcher); +} + +} // namespace FlatTree +} // namespace chip diff --git a/src/lib/format/FlatTreePosition.h b/src/lib/format/FlatTreePosition.h new file mode 100644 index 00000000000000..e01f8b46abf702 --- /dev/null +++ b/src/lib/format/FlatTreePosition.h @@ -0,0 +1,193 @@ +/* + * + * Copyright (c) 2023 Project CHIP Authors + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include +#include + +namespace chip { +namespace FlatTree { + +/// Represents a position inside a given tree, allowing for descending +/// and ascending. +/// +/// A possition in the tree may be undefined if descending to a non-existing leaf, +/// however the position still allows moving back again. +/// +/// DESCEND_DEPTH is the maximum remembered depth for going back up. +/// +/// General usage: +/// +/// Position position(tree, tree_size); +/// +/// position.Enter(ByName("foo")); +/// position.Enter(ByName("bar")); +/// position.Enter(ByName("baz")); +/// +/// position.Get() /// content of foo::bar::baz if it exists +/// +/// position.Exit(); +/// position.Exit(); +/// +/// position.Get() /// content of foo if it exists +/// +/// position.Enter(ById(1234)); +/// +/// position.Get() /// content of foo::1234 +/// +template +class Position +{ +public: + Position(const Node * tree, size_t treeSize) : mTree(tree), mTreeSize(treeSize) {} + + template + Position(const std::array, N> & tree) : mTree(tree.data()), mTreeSize(N) + {} + + // Move back to the top + void ResetToTop() + { + mDescendDepth = 0; + mUnknownDescendDepth = 0; + } + + /// Attempt to find a child of the current position that matches + /// the given matcher + template + void Enter(MATCHER matcher); + + /// Move up the tree, undoes an 'Enter' operation. + void Exit(); + + /// Fetch the value where the node is positioned on or nullptr if that + /// value is not available; + const CONTENT * Get() const; + + /// Returns the entries visited so far + /// + /// WILL RETURN EMPTY if the descend depth has been + /// exceeded. Callers MUST handle empty return. + /// + /// Span valid until one of Enter/Exit functions are called + /// and as long as the Position is valid (points inside the object). + chip::Span *> CurrentPath(); + + bool HasValidTree() const { return mTree != nullptr; } + + size_t DescendDepth() const { return mDescendDepth + mUnknownDescendDepth; } + +private: + // actual tree that we visit + const Node * mTree = nullptr; + const size_t mTreeSize = 0; + + // Keeping track of descending into the tree, to be able to move back + // last element in the array is the "current" item + const Entry * mPositions[DESCEND_DEPTH] = { nullptr }; + size_t mDescendDepth = 0; // filled amount of mDescendPositions + + // Descend past remembering memory or in not-found entries. + size_t mUnknownDescendDepth = 0; // depth in invalid positions +}; + +template +const CONTENT * Position::Get() const +{ + if (mUnknownDescendDepth > 0) + { + return nullptr; + } + + if (mDescendDepth == 0) + { + return nullptr; + } + + return &mPositions[mDescendDepth - 1]->data; +} + +template +template +void Position::Enter(MATCHER matcher) +{ + if (mUnknownDescendDepth > 0) + { + // keep descending into the unknown + mUnknownDescendDepth++; + return; + } + + // To be able to descend, we have to be able to remember + // the current position + if (mDescendDepth == DESCEND_DEPTH) + { + mUnknownDescendDepth = 1; + return; + } + + size_t nodeIdx = 0; // assume root node + if (mDescendDepth > 0) + { + nodeIdx = mPositions[mDescendDepth - 1]->node_index; + } + + const Entry * child = FindEntry(mTree, mTreeSize, nodeIdx, matcher); + + if (child == nullptr) + { + mUnknownDescendDepth = 1; + return; + } + + mPositions[mDescendDepth++] = child; +} + +template +void Position::Exit() +{ + if (mUnknownDescendDepth > 0) + { + mUnknownDescendDepth--; + return; + } + + if (mDescendDepth > 0) + { + mDescendDepth--; + } +} + +template +chip::Span *> Position::CurrentPath() +{ + if (mUnknownDescendDepth > 0) + { + return chip::Span *>(); + } + + // const chip::FlatTree::Entry<{anonymous}::NamedTag>* const* to + // const chip::FlatTree::Entry<{anonymous}::NamedTag>** + typename chip::Span *>::pointer p = mPositions; + + // return chip::Span *>(mPositions, mDescendDepth); + return chip::Span *>(p, mDescendDepth); +} + +} // namespace FlatTree +} // namespace chip diff --git a/src/lib/format/protocol_decoder.cpp b/src/lib/format/protocol_decoder.cpp new file mode 100644 index 00000000000000..8c3b352133902a --- /dev/null +++ b/src/lib/format/protocol_decoder.cpp @@ -0,0 +1,657 @@ +/* + * + * Copyright (c) 2023 Project CHIP Authors + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include + +#include +#include + +namespace chip { +namespace Decoders { + +namespace { + +using namespace chip::TLV; + +using chip::StringBuilderBase; +using chip::TLVMeta::AttributeTag; +using chip::TLVMeta::ClusterTag; +using chip::TLVMeta::CommandTag; +using chip::TLVMeta::ConstantValueTag; +using chip::TLVMeta::EventTag; +using chip::TLVMeta::ItemType; + +class ByTag +{ +public: + constexpr ByTag(Tag tag) : mTag(tag) {} + bool operator()(const chip::TLVMeta::ItemInfo & item) { return item.tag == mTag; } + +private: + const Tag mTag; +}; + +const char * DecodeTagControl(const TLVTagControl aTagControl) +{ + switch (aTagControl) + { + case TLVTagControl::Anonymous: + return "Anonymous"; + case TLVTagControl::ContextSpecific: + return "ContextSpecific"; + case TLVTagControl::CommonProfile_2Bytes: + return "Common2B"; + case TLVTagControl::CommonProfile_4Bytes: + return "Common4B"; + case TLVTagControl::ImplicitProfile_2Bytes: + return "Implicit2B"; + case TLVTagControl::ImplicitProfile_4Bytes: + return "Implicit4B"; + case TLVTagControl::FullyQualified_6Bytes: + return "FullyQualified6B"; + case TLVTagControl::FullyQualified_8Bytes: + return "FullyQualified8"; + default: + return "???"; + } +} + +void FormatCurrentTag(const TLVReader & reader, chip::StringBuilderBase & out) +{ + chip::TLV::TLVTagControl tagControl = static_cast(reader.GetControlByte() & kTLVTagControlMask); + chip::TLV::Tag tag = reader.GetTag(); + + if (IsProfileTag(tag)) + { + out.AddFormat("%s(0x%X::0x%X::0x%" PRIX32 ")", DecodeTagControl(tagControl), VendorIdFromTag(tag), ProfileNumFromTag(tag), + TagNumFromTag(tag)); + } + else if (IsContextTag(tag)) + { + out.AddFormat("%s(0x%" PRIX32 ")", DecodeTagControl(tagControl), TagNumFromTag(tag)); + } + else + { + out.AddFormat("UnknownTag(0x%" PRIX64 ")", tag.RawValue()); + } +} + +CHIP_ERROR FormatCurrentValue(TLVReader & reader, chip::StringBuilderBase & out) +{ + switch (reader.GetType()) + { + case kTLVType_SignedInteger: { + int64_t sVal; + ReturnErrorOnFailure(reader.Get(sVal)); + out.AddFormat("%" PRIi64, sVal); + break; + } + case kTLVType_UnsignedInteger: { + uint64_t uVal; + ReturnErrorOnFailure(reader.Get(uVal)); + out.AddFormat("%" PRIu64, uVal); + break; + } + case kTLVType_Boolean: { + bool bVal; + ReturnErrorOnFailure(reader.Get(bVal)); + out.Add(bVal ? "true" : "false"); + break; + } + case kTLVType_FloatingPointNumber: { + double fpVal; + ReturnErrorOnFailure(reader.Get(fpVal)); + out.AddFormat("%lf", fpVal); + break; + } + case kTLVType_UTF8String: { + const uint8_t * strbuf = nullptr; + ReturnErrorOnFailure(reader.GetDataPtr(strbuf)); + out.AddFormat("\"%-.*s\"", static_cast(reader.GetLength()), strbuf); + break; + } + case kTLVType_ByteString: { + const uint8_t * strbuf = nullptr; + ReturnErrorOnFailure(reader.GetDataPtr(strbuf)); + out.Add("hex:"); + for (uint32_t i = 0; i < reader.GetLength(); i++) + { + out.AddFormat("%02X", strbuf[i]); + } + break; + } + case kTLVType_Null: + out.Add("NULL"); + break; + + case kTLVType_NotSpecified: + out.Add("Not Specified"); + break; + default: + out.Add("???"); + break; + } + + return CHIP_NO_ERROR; +} + +// Returns a null terminated string containing the current reader value +void PrettyPrintCurrentValue(TLVReader & reader, chip::StringBuilderBase & out, PayloadDecoderBase::DecodePosition & position) +{ + CHIP_ERROR err = FormatCurrentValue(reader, out); + + if (err != CHIP_NO_ERROR) + { + out.AddFormat("ERR: %" CHIP_ERROR_FORMAT, err.Format()); + return; + } + + auto data = position.Get(); + if (data == nullptr) + { + return; + } + + // Report enum values in human readable form + if (data->type == ItemType::kEnum && (reader.GetType() == kTLVType_UnsignedInteger)) + { + uint64_t value = 0; + VerifyOrReturn(reader.Get(value) == CHIP_NO_ERROR); + + position.Enter(ByTag(ConstantValueTag(value))); + auto enum_data = position.Get(); + if (enum_data != nullptr) + { + out.Add(" == ").Add(enum_data->name); + } + position.Exit(); + } + + if (data->type == ItemType::kBitmap && (reader.GetType() == kTLVType_UnsignedInteger)) + { + uint64_t value = 0; + VerifyOrReturn(reader.Get(value) == CHIP_NO_ERROR); + + uint64_t bit = 0x01; + bool first = true; + for (unsigned i = 0; i < 64; i++, bit <<= 1) + { + if ((value & bit) == 0) + { + continue; + } + + // NOTE: this only can select individual bits; + position.Enter(ByTag(ConstantValueTag(bit))); + auto bitmap_data = position.Get(); + if (bitmap_data == nullptr) + { + position.Exit(); + continue; + } + + // Try to pretty print the value + if (first) + { + out.Add(" == "); + first = false; + } + else + { + out.Add(" | "); + } + + out.Add(bitmap_data->name); + value = value & (~bit); + + position.Exit(); + } + + if (!first && value) + { + // Only append if some constants were found. + out.AddFormat(" | 0x%08" PRIX64, value); + } + } +} + +} // namespace + +PayloadDecoderBase::PayloadDecoderBase(const PayloadDecoderInitParams & params, StringBuilderBase & nameBuilder, + StringBuilderBase & valueBuilder) : + mProtocol(params.GetProtocol()), + mMessageType(params.GetMessageType()), mNameBuilder(nameBuilder), mValueBuilder(valueBuilder), + + mPayloadPosition(params.GetProtocolDecodeTree(), params.GetProtocolDecodeTreeSize()), + mIMContentPosition(params.GetClusterDecodeTree(), params.GetClusterDecodeTreeSize()) +{} + +void PayloadDecoderBase::StartDecoding(const TLVReader & reader) +{ + mReader = reader; + mPayloadPosition.ResetToTop(); + mIMContentPosition.ResetToTop(); + mCurrentNesting = 0; + mClusterId = 0; + mAttributeId = 0; + mEventId = 0; + mCommandId = 0; + mState = State::kStarting; +} + +bool PayloadDecoderBase::Next(PayloadEntry & entry) +{ + switch (mState) + { + case State::kStarting: + NextFromStarting(entry); + return true; + case State::kValueRead: + NextFromValueRead(entry); + return true; + case State::kContentRead: + NextFromContentRead(entry); + return true; + case State::kDone: + return false; + } + // should never happen + return false; +} + +void PayloadDecoderBase::NextFromStarting(PayloadEntry & entry) +{ + // Find the right protocol (fake cluster id) + mPayloadPosition.Enter(ByTag(ClusterTag(0xFFFF0000 | mProtocol.ToFullyQualifiedSpecForm()))); + mPayloadPosition.Enter(ByTag(AttributeTag(mMessageType))); + + auto data = mPayloadPosition.Get(); + if (data == nullptr) + { + // do not try to decode unknown data. assume binary + mNameBuilder.Reset().AddFormat("PROTO(0x%" PRIX32 ", 0x%X)", mProtocol.ToFullyQualifiedSpecForm(), mMessageType); + mValueBuilder.Reset().Add("UNKNOWN"); + entry = PayloadEntry::SimpleValue(mNameBuilder.c_str(), mValueBuilder.c_str()); + mState = State::kDone; + return; + } + + // name is known (so valid protocol) + if (mReader.GetTotalLength() == 0) + { + mState = State::kDone; + entry = PayloadEntry::SimpleValue(data->name, ""); + return; + } + + if (data->type == ItemType::kProtocolBinaryData) + { + mState = State::kDone; + entry = PayloadEntry::SimpleValue(data->name, "BINARY DATA"); + return; + } + + CHIP_ERROR err = mReader.Next(kTLVType_Structure, AnonymousTag()); + if (err != CHIP_NO_ERROR) + { + mValueBuilder.Reset().AddFormat("ERROR getting Anonymous Structure TLV: %" CHIP_ERROR_FORMAT, err.Format()); + mState = State::kDone; + entry = PayloadEntry::SimpleValue(data->name, mValueBuilder.c_str()); + return; + } + + EnterContainer(entry); +} + +void PayloadDecoderBase::ExitContainer(PayloadEntry & entry) +{ + entry = PayloadEntry::NestingExit(); + + if (mCurrentNesting > 0) + { + if (mState == State::kContentRead) + { + mIMContentPosition.Exit(); + if (mIMContentPosition.DescendDepth() <= 1) + { + // Lever for actual content is 2: cluster::attr/cmd/ev + mState = State::kValueRead; + mPayloadPosition.Exit(); + } + } + else + { + mPayloadPosition.Exit(); + } + mReader.ExitContainer(mNestingEnters[--mCurrentNesting]); + } + + if (mCurrentNesting == 0) + { + mState = State::kDone; + } +} + +bool PayloadDecoderBase::ReaderEnterContainer(PayloadEntry & entry) +{ + if (mCurrentNesting >= kMaxDecodeDepth) + { + mValueBuilder.AddFormat("NESTING DEPTH REACHED"); + entry = PayloadEntry::SimpleValue(mNameBuilder.c_str(), mValueBuilder.c_str()); + return false; + } + + TLVType containerType; + CHIP_ERROR err = mReader.EnterContainer(containerType); + if (err != CHIP_NO_ERROR) + { + mValueBuilder.AddFormat("ERROR entering container: %" CHIP_ERROR_FORMAT, err.Format()); + entry = PayloadEntry::SimpleValue(mNameBuilder.c_str(), mValueBuilder.c_str()); + mState = State::kDone; + return false; + } + + mNestingEnters[mCurrentNesting++] = containerType; + + return true; +} + +void PayloadDecoderBase::EnterContainer(PayloadEntry & entry) +{ + // must be done BEFORE entering container + // to preserve the value and not get a 'container tag' + // below when data is not valid + // + // TODO: this formatting is wasteful, should really be done only + // if data is NULLPTR. + FormatCurrentTag(mReader, mNameBuilder.Reset()); + + VerifyOrReturn(ReaderEnterContainer(entry)); + + const chip::TLVMeta::ItemInfo * data = nullptr; + + if (mState == State::kContentRead) + { + data = mIMContentPosition.Get(); + } + else + { + mState = State::kValueRead; + data = mPayloadPosition.Get(); + } + + if (data == nullptr) + { + entry = PayloadEntry::NestingEnter(mNameBuilder.c_str()); + } + else + { + entry = PayloadEntry::NestingEnter(data->name); + } +} + +void PayloadDecoderBase::NextFromContentRead(PayloadEntry & entry) +{ + CHIP_ERROR err = mReader.Next(); + if (err == CHIP_END_OF_TLV) + { + ExitContainer(entry); + return; + } + + if (err != CHIP_NO_ERROR) + { + mValueBuilder.Reset().AddFormat("ERROR on TLV Next: %" CHIP_ERROR_FORMAT, err.Format()); + entry = PayloadEntry::SimpleValue("TLV_ERR", mValueBuilder.c_str()); + mState = State::kDone; + return; + } + + if (mCurrentNesting > 0 && mNestingEnters[mCurrentNesting - 1] == kTLVType_List) + { + // Spec A5.3: `The members of a list may be encoded with any form of tag, including an anonymous tag.` + // TLVMeta always uses Anonymous + mIMContentPosition.Enter(ByTag(AnonymousTag())); + } + else + { + mIMContentPosition.Enter(ByTag(mReader.GetTag())); + } + auto data = mIMContentPosition.Get(); + + if (data != nullptr) + { + if (data->type == ItemType::kProtocolBinaryData) + { + mIMContentPosition.Exit(); + entry = PayloadEntry::SimpleValue(data->name, "BINARY DATA"); + return; + } + } + + if (TLVTypeIsContainer(mReader.GetType())) + { + EnterContainer(entry); + return; + } + + PrettyPrintCurrentValue(mReader, mValueBuilder.Reset(), mIMContentPosition); + mIMContentPosition.Exit(); + + if (data == nullptr) + { + FormatCurrentTag(mReader, mNameBuilder.Reset()); + entry = PayloadEntry::SimpleValue(mNameBuilder.c_str(), mValueBuilder.c_str()); + return; + } + + entry = PayloadEntry::SimpleValue(data->name, mValueBuilder.c_str()); +} + +void PayloadDecoderBase::MoveToContent(PayloadEntry & entry) +{ + if (!mIMContentPosition.HasValidTree()) + { + mPayloadPosition.Exit(); + return; + } + + VerifyOrDie((entry.GetType() == PayloadEntry::IMPayloadType::kAttribute) || + (entry.GetType() == PayloadEntry::IMPayloadType::kCommand) || + (entry.GetType() == PayloadEntry::IMPayloadType::kEvent)); + + mNameBuilder.Reset(); + + mIMContentPosition.ResetToTop(); + mIMContentPosition.Enter(ByTag(ClusterTag(entry.GetClusterId()))); + auto data = mIMContentPosition.Get(); + if (data != nullptr) + { + mNameBuilder.AddFormat("%s::", data->name); + } + else + { + mNameBuilder.AddFormat("0x%" PRIx32 "::", entry.GetClusterId()); + } + + uint32_t id = 0; + const char * id_type = "UNKNOWN"; + + switch (entry.GetType()) + { + case PayloadEntry::IMPayloadType::kAttribute: + mIMContentPosition.Enter(ByTag(AttributeTag(entry.GetAttributeId()))); + id = entry.GetAttributeId(); + id_type = "ATTR"; + break; + case PayloadEntry::IMPayloadType::kCommand: + mIMContentPosition.Enter(ByTag(CommandTag(entry.GetCommandId()))); + id = entry.GetCommandId(); + id_type = "CMD"; + break; + case PayloadEntry::IMPayloadType::kEvent: + mIMContentPosition.Enter(ByTag(EventTag(entry.GetEventId()))); + id = entry.GetEventId(); + id_type = "EV"; + break; + default: + // never happens: verified all case above covered. + break; + } + + data = mIMContentPosition.Get(); + if (data != nullptr) + { + mNameBuilder.AddFormat("%s", data->name); + } + else + { + mNameBuilder.AddFormat("%s(0x%" PRIx32 ")", id_type, id); + } + + if (TLVTypeIsContainer(mReader.GetType())) + { + mState = State::kContentRead; + entry = PayloadEntry::NestingEnter(mNameBuilder.c_str()); + ReaderEnterContainer(entry); + } + else + { + PrettyPrintCurrentValue(mReader, mValueBuilder.Reset(), mIMContentPosition); + entry = PayloadEntry::SimpleValue(mNameBuilder.c_str(), mValueBuilder.c_str()); + + // Can simply exit, only one value to return + mPayloadPosition.Exit(); + } +} + +void PayloadDecoderBase::NextFromValueRead(PayloadEntry & entry) +{ + CHIP_ERROR err = mReader.Next(); + if (err == CHIP_END_OF_TLV) + { + ExitContainer(entry); + return; + } + + if (err != CHIP_NO_ERROR) + { + mValueBuilder.Reset().AddFormat("ERROR on TLV Next: %" CHIP_ERROR_FORMAT, err.Format()); + entry = PayloadEntry::SimpleValue("TLV_ERR", mValueBuilder.c_str()); + mState = State::kDone; + return; + } + + // Attempt to find information about the current tag + mPayloadPosition.Enter(ByTag(mReader.GetTag())); + auto data = mPayloadPosition.Get(); + + // handle special types + if (data != nullptr) + { + if (data->type == ItemType::kProtocolBinaryData) + { + mPayloadPosition.Exit(); + entry = PayloadEntry::SimpleValue(data->name, "BINARY DATA"); + return; + } + + if (data->type == ItemType::kProtocolPayloadAttribute) + { + entry = PayloadEntry::AttributePayload(mClusterId, mAttributeId); + MoveToContent(entry); + return; + } + + if (data->type == ItemType::kProtocolPayloadCommand) + { + entry = PayloadEntry::CommandPayload(mClusterId, mCommandId); + MoveToContent(entry); + return; + } + + if (data->type == ItemType::kProtocolPayloadEvent) + { + entry = PayloadEntry::EventPayload(mClusterId, mEventId); + MoveToContent(entry); + return; + } + } + + if (TLVTypeIsContainer(mReader.GetType())) + { + EnterContainer(entry); + return; + } + + if (data == nullptr) + { + FormatCurrentTag(mReader, mNameBuilder.Reset()); + PrettyPrintCurrentValue(mReader, mValueBuilder.Reset(), mPayloadPosition); + entry = PayloadEntry::SimpleValue(mNameBuilder.c_str(), mValueBuilder.c_str()); + mPayloadPosition.Exit(); + return; + } + + // at this point, data is "simple data" or "simple data with meaning" + + const chip::TLVMeta::ItemInfo * info = nullptr; + switch (data->type) + { + case ItemType::kProtocolClusterId: + mReader.Get(mClusterId); + mIMContentPosition.ResetToTop(); + mIMContentPosition.Enter(ByTag(ClusterTag(mClusterId))); + info = mIMContentPosition.Get(); + break; + case ItemType::kProtocolAttributeId: + mReader.Get(mAttributeId); + mIMContentPosition.ResetToTop(); + mIMContentPosition.Enter(ByTag(ClusterTag(mClusterId))); + mIMContentPosition.Enter(ByTag(AttributeTag(mAttributeId))); + info = mIMContentPosition.Get(); + break; + case ItemType::kProtocolCommandId: + mReader.Get(mCommandId); + mIMContentPosition.ResetToTop(); + mIMContentPosition.Enter(ByTag(ClusterTag(mClusterId))); + mIMContentPosition.Enter(ByTag(CommandTag(mCommandId))); + info = mIMContentPosition.Get(); + break; + case ItemType::kProtocolEventId: + mReader.Get(mEventId); + mIMContentPosition.ResetToTop(); + mIMContentPosition.Enter(ByTag(ClusterTag(mClusterId))); + mIMContentPosition.Enter(ByTag(EventTag(mEventId))); + info = mIMContentPosition.Get(); + break; + default: + break; + } + + PrettyPrintCurrentValue(mReader, mValueBuilder.Reset(), mPayloadPosition); + if (info != nullptr) + { + mValueBuilder.Add(" == '").Add(info->name).Add("'"); + } + + mPayloadPosition.Exit(); + entry = PayloadEntry::SimpleValue(data->name, mValueBuilder.c_str()); +} + +} // namespace Decoders +} // namespace chip diff --git a/src/lib/format/protocol_decoder.h b/src/lib/format/protocol_decoder.h new file mode 100644 index 00000000000000..3cf3cdb2183780 --- /dev/null +++ b/src/lib/format/protocol_decoder.h @@ -0,0 +1,289 @@ +/* + * + * Copyright (c) 2023 Project CHIP Authors + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include +#include +#include +#include +#include + +namespace chip { +namespace Decoders { + +/// Represents an individual decoded entry for IM Payloads +/// Generally a name + value + metadata tuple, where name and value are never NULL. +class PayloadEntry +{ +public: + static constexpr uint32_t kInvalidId = 0xFFFFFFFF; + enum class IMPayloadType + { + kNone = 0, // generally should not be used except initial init + kValue, // represents a simple value to output + + kNestingEnter, // Nested struct enter. Has name, empty value + kNestingExit, // Nested struct exit (has no name/value) + + // Content payloads + kAttribute, + kCommand, + kEvent, + }; + PayloadEntry(const PayloadEntry &) = default; + PayloadEntry & operator=(const PayloadEntry &) = default; + + PayloadEntry() : mType(IMPayloadType::kNone), mName(""), mValue("") {} + + IMPayloadType GetType() const { return mType; } + + const char * GetName() const { return mName; } + const char * GetValueText() const { return mValue; } + + /// valid only if payload is an IM Payload + uint32_t GetClusterId() const { return mClusterId; }; + + /// valid only if payload as an Attribute ID + uint32_t GetAttributeId() const + { + VerifyOrReturnValue(mType == IMPayloadType::kAttribute, kInvalidId); + return mSubId; + } + + /// valid only if payload as a Command ID + uint32_t GetCommandId() const + { + VerifyOrReturnValue(mType == IMPayloadType::kCommand, kInvalidId); + return mSubId; + } + + /// valid only if payload as a Command ID + uint32_t GetEventId() const + { + VerifyOrReturnValue(mType == IMPayloadType::kEvent, kInvalidId); + return mSubId; + } + + static PayloadEntry SimpleValue(const char * name, const char * value) + { + return PayloadEntry(IMPayloadType::kValue, name, value); + } + + static PayloadEntry NestingEnter(const char * name) { return PayloadEntry(IMPayloadType::kNestingEnter, name, ""); } + + static PayloadEntry NestingExit() { return PayloadEntry(IMPayloadType::kNestingExit, "", ""); } + + static PayloadEntry AttributePayload(uint32_t cluster_id, uint32_t attribute_id) + { + PayloadEntry result(IMPayloadType::kAttribute, "ATTR_DATA", ""); + result.mClusterId = cluster_id; + result.mSubId = attribute_id; + + return result; + } + + static PayloadEntry CommandPayload(uint32_t cluster_id, uint32_t command_id) + { + PayloadEntry result(IMPayloadType::kCommand, "COMMAND_DATA", ""); + result.mClusterId = cluster_id; + result.mSubId = command_id; + return result; + } + + static PayloadEntry EventPayload(uint32_t cluster_id, uint32_t event_id) + { + PayloadEntry result(IMPayloadType::kEvent, "EVENT_DATA", ""); + result.mClusterId = cluster_id; + result.mSubId = event_id; + return result; + } + +private: + PayloadEntry(IMPayloadType type, const char * name, const char * value) : mType(type), mName(name), mValue(value) {} + + IMPayloadType mType = IMPayloadType::kValue; + const char * mName = nullptr; + const char * mValue = nullptr; + + uint32_t mClusterId = 0; + uint32_t mSubId = 0; // attribute, command or event id +}; + +/// Sets up decoding of some Matter data payload +class PayloadDecoderInitParams +{ +public: + using DecodeTree = const FlatTree::Node *; + PayloadDecoderInitParams() = default; + + PayloadDecoderInitParams & SetProtocol(Protocols::Id value) + { + mProtocol = value; + return *this; + } + + PayloadDecoderInitParams & SetMessageType(uint8_t value) + { + mMessageType = value; + return *this; + } + + PayloadDecoderInitParams & SetProtocolDecodeTree(DecodeTree tree, size_t s) + { + mProtocolTree = tree; + mProtocolTreeSize = s; + return *this; + } + + template + PayloadDecoderInitParams & SetProtocolDecodeTree(const std::array, N> & a) + { + return SetProtocolDecodeTree(a.data(), N); + } + + PayloadDecoderInitParams & SetClusterDecodeTree(DecodeTree tree, size_t s) + { + mClusterTree = tree; + mClusterTreeSize = s; + return *this; + } + + template + PayloadDecoderInitParams & SetClusterDecodeTree(const std::array, N> & a) + { + return SetClusterDecodeTree(a.data(), N); + } + + DecodeTree GetProtocolDecodeTree() const { return mProtocolTree; } + size_t GetProtocolDecodeTreeSize() const { return mProtocolTreeSize; } + DecodeTree GetClusterDecodeTree() const { return mClusterTree; } + size_t GetClusterDecodeTreeSize() const { return mClusterTreeSize; } + + Protocols::Id GetProtocol() const { return mProtocol; } + uint8_t GetMessageType() const { return mMessageType; } + +private: + DecodeTree mProtocolTree = nullptr; + size_t mProtocolTreeSize = 0; + + DecodeTree mClusterTree = nullptr; + size_t mClusterTreeSize = 0; + + Protocols::Id mProtocol = Protocols::NotSpecified; + uint8_t mMessageType = 0; +}; + +class PayloadDecoderBase +{ +public: + static constexpr size_t kMaxDecodeDepth = 16; + using DecodePosition = chip::FlatTree::Position; + + PayloadDecoderBase(const PayloadDecoderInitParams & params, StringBuilderBase & nameBuilder, StringBuilderBase & valueBuilder); + + /// Initialize decoding from the given reader + /// Will create a copy of the reader, however the copy will contain + /// pointers in the original reader (so data must stay valid while Next is called) + void StartDecoding(const TLV::TLVReader & reader); + + void StartDecoding(chip::ByteSpan data) + { + TLV::TLVReader reader; + reader.Init(data); + StartDecoding(reader); + } + + /// Get the next decoded entry from the underlying TLV + /// + /// If a cluster decoder is set, then kAttribute/kCommand/kEvent are ALWAYS decoded + /// (even if unknown tags), otherwise they will be returned as separate PayloadEntry values. + /// + /// Returns false when decoding finished. + bool Next(PayloadEntry & entry); + + const TLV::TLVReader & ReadState() const { return mReader; } + +private: + enum class State + { + kStarting, + kValueRead, // reading value for Payload + kContentRead, // reading value for IMContent (i.e. decoded attr/cmd/event) + kDone, + }; + + /// Move to the given attribute/event/command entry. + /// + /// [entry] MUST be of type command/attribute/event. + /// + /// This call either moves to "ContentDecoding mode" if content tree is available + /// or leaves entry unchanged if content decoding tree is not available. + void MoveToContent(PayloadEntry & entry); + + void NextFromStarting(PayloadEntry & entry); + void NextFromValueRead(PayloadEntry & entry); + void NextFromContentRead(PayloadEntry & entry); + + /// Enter the container in mReader. + /// + /// May change entry/state in case of errors. + /// + /// Returns false on error (and entry is changed in that case) + bool ReaderEnterContainer(PayloadEntry & entry); + + /// Returns a "nesting enter" value, making sure that + /// nesting depth is sufficient. + void EnterContainer(PayloadEntry & entry); + + /// Returns a "nesting exit" value, making sure that + /// nesting depth is sufficient. + void ExitContainer(PayloadEntry & entry); + + const chip::Protocols::Id mProtocol; + const uint8_t mMessageType; + + StringBuilderBase & mNameBuilder; + StringBuilderBase & mValueBuilder; + + State mState = State::kStarting; + TLV::TLVReader mReader; + DecodePosition mPayloadPosition; + DecodePosition mIMContentPosition; + TLV::TLVType mNestingEnters[kMaxDecodeDepth]; + size_t mCurrentNesting = 0; + + /// incremental state for parsing of paths + uint32_t mClusterId = 0; + uint32_t mAttributeId = 0; + uint32_t mEventId = 0; + uint32_t mCommandId = 0; +}; + +template +class PayloadDecoder : public PayloadDecoderBase +{ +public: + PayloadDecoder(const PayloadDecoderInitParams & params) : PayloadDecoderBase(std::move(params), mName, mValue) {} + +private: + chip::StringBuilder mName; + chip::StringBuilder mValue; +}; + +} // namespace Decoders +} // namespace chip diff --git a/src/lib/format/protocol_messages.matter b/src/lib/format/protocol_messages.matter new file mode 100644 index 00000000000000..0346b138fd45b0 --- /dev/null +++ b/src/lib/format/protocol_messages.matter @@ -0,0 +1,406 @@ +/// Fake cluster for defining structures for secure channel protocol +/// +/// This follows the `4.10.1. Secure Channel Protocol Messages` +/// defined in the Matter specification. +/// +/// This is NOT a real cluster. +/// +/// Since this is a protocol decoder, the following CUSTOM (non-zap) types +/// Are used as markers for data: +/// +/// Protocol decoding paths: +/// protocol_cluster_id +/// protocol_attribute_id +/// protocol_command_id +/// protocol_event_id +/// +/// Payloads: +/// cluster_attribute_payload +/// cluster_command_payload +/// cluster_event_payload +/// +/// Special types: +/// protocol_binary_data - structures without TLV encoded data +/// +client cluster SecureChannelProtocol = 0xFFFF0000 { + + struct ICDParameterStruct { + int32u sleepy_idle_interval = 1; + int32u sleepy_active_interval = 2; + } + + struct PBKDFParamRequest { + octet_string<32> initiator_random = 1; + int16u initiator_session_id = 2; + int16u passcode_id = 3; + boolean has_pbkdf_parameters = 4; + optional ICDParameterStruct initiator_icd_params = 5; + } + + struct CryptoPBKDFParameterSet { + int32u iterations = 1; + octet_string<32> salt = 2; // 16..32 in size + } + + struct PBKDFParamResponse { + octet_string<32> initiator_random = 1; + octet_string<32> responder_random = 2; + int16u responder_session_id = 3; + CryptoPBKDFParameterSet pbkdf_parameters = 4; + optional IDCParameterStruct responder_icd_params = 5; + } + + struct PasePake1 { + octet_string pA = 1; + } + + struct PasePake2 { + octet_string pB = 1; + octet_string cB = 2; + } + + struct PasePake3 { + octet_string cA = 1; + } + + struct CaseSigma1 { + octet_string<32> initiator_random = 1; + int16u initiator_session_id = 2; + octet_string<32> destination_id = 3; + octet_string<65> initiator_eph_pub_key = 4; + optional ICDParameterStruct initiator_icd_params = 5; + optional octet_string<16> resumption_id = 6; + optional octet_string initiator_resume_mic = 7; + } + + struct CaseSigma2 { + octet_string<32> responder_random = 1; + int16u responder_sessoion_id = 2; + octet_string<65> responder_eph_pub_key = 3; + octet_string encrypted2 = 4; + optional ICDParameterStruct responder_icd_params = 5; + } + + struct CaseSigma3 { + octet_string encrypted = 1; + } + + struct CaseSigma2Resume { + octet_string<16> resumption_id = 1; + octet_string<16> sigma2_resume_mic = 2; + int16u responder_sessoion_id = 3; + optional ICDParameterStruct responder_icd_params = 4; + } + + // IDs here are based on Protocol opcodes + // + // Written here as "attributes" to force code generation + // to consider these active structures. + readonly attribute protocol_binary_data msg_counter_sync_request = 0x00; + readonly attribute protocol_binary_data msg_counter_sync_response = 0x01; + readonly attribute protocol_binary_data mrp_ack = 0x10; + readonly attribute PBKDFParamRequest pbkdf_param_request = 0x20; + readonly attribute PBKDFParamResponse pbkdf_param_response = 0x21; + readonly attribute PasePake1 pase_pake1 = 0x22; + readonly attribute PasePake2 pase_pake2 = 0x23; + readonly attribute PasePake3 pase_pake3 = 0x24; + readonly attribute CaseSigma1 case_sigma1 = 0x30; + readonly attribute CaseSigma2 case_sigma2 = 0x31; + readonly attribute CaseSigma3 case_sigma3 = 0x32; + readonly attribute CaseSigma2Resume case_sigma2_resume = 0x33; + readonly attribute protocol_binary_data status_report = 0x40; +} + +/// Fake cluster for defining structures for IM data encoding. +/// +/// This follows the `10. Interaction Model Encoding Specification` +/// defined in the Matter specification. +/// +/// This is NOT a real cluster. +/// +client cluster IMProtocol = 0xFFFF0001 { + struct AttributePathIB { + boolean enable_tag_compression = 0; + optional int64u node_id = 1; + optional int16u endpoint_id = 2; + optional protocol_cluster_id cluster_id = 3; + optional protocol_attribute_id attribute_id = 4; + optional nullable int16u list_index = 5; + } + + struct EventPathIB { + optional int64u node_id = 0; + optional int16u endpoint_id = 1; + optional protocol_cluster_id cluster_id = 2; + optional protocol_event_id event_id = 3; + boolean is_urgent = 4; + } + + struct EventFilterIB { + optional int64u node_id = 0; + int64u event_min = 1; + } + + struct ClusterPathIB { + optional int64u node_id = 0; + optional int16u endpoint_id = 1; + optional protocol_cluster_id cluster_id = 2; + } + + struct DataVersionFilterIB { + ClusterPathIB path = 0; + int32u data_version = 1; + } + + struct StatusResponseMessage { + int8u status = 0; + + // 10.2.2.2. Context Tag Encoded Action Information + int8u interaction_model_revison = 0xFF; + } + + struct ReadRequestMessage { + optional AttributePathIB attribute_requests[] = 0; + optional EventPathIB event_requests[] = 1; + optional EventFilterIB event_filters[] = 2; + optional boolean fabric_filtered = 3; + optional DataVersionFilterIB data_version_filters[] = 4; + + // 10.2.2.2. Context Tag Encoded Action Information + int8u interaction_model_revison = 0xFF; + } + + struct SubscribeRequestMessage { + boolean keep_subscriptions = 0; + int16u min_minterval_floor = 1; + int16u max_minterval_ceiling = 2; + optional AttributePathIB attribute_requests = 3; + optional EventPathIB event_requests = 4; + optional EventFilterIB event_filters = 5; + // NOTE: 6 is missing here ... + boolean fabric_filtered = 7; + optional DataVersionFilterIB data_version_filters = 8; + + // 10.2.2.2. Context Tag Encoded Action Information + int8u interaction_model_revison = 0xFF; + } + + struct SubscribeResponseMessage { + int32u subscription_id = 0; + // NOTE: 1 missing here + int16u max_interval = 2; + + // 10.2.2.2. Context Tag Encoded Action Information + int8u interaction_model_revison = 0xFF; + } + + enum StatusCodeEnum : ENUM8 { + kSuccess = 0x0; + kFailure = 0x01; + kInvalidSubscription = 0x7d; + kUnsupportedAccess = 0x7e; + kUnsupportedEndpoint = 0x7f; + kInvalidAction = 0x80; + kUnsupportedCommand = 0x81; + kInvalidCommand = 0x85; + kUnsupportedAttribute = 0x86; + kConstraintError = 0x87; + kUnsupportedWrite = 0x88; + kResourceExhausted = 0x89; + kNotFound = 0x8b; + kUnreportableAttribute = 0x8c; + kInvalidDataType = 0x8d; + kUnsupportedRead = 0x8f; + kDataVersionMismatch = 0x92; + kTimeout = 0x94; + kBusy = 0x9c; + kUnsupportedCluster = 0xc3; + kNoUpstreamSubscription = 0xc5; + kNeedsTimedInteraction = 0xc6; + kUnsupportedEvent = 0xc7; + kPathsExhausted = 0xc8; + kTimedRequestMismatch = 0xc9; + kFailsafeRequired = 0xca; + kWriteIgnored = 0xF0; + } + + struct StatusIB { + StatusCodeEnum status = 0; + int8u cluster_status = 1; + } + + struct AttributeStatus { + AttributePathIB path = 0; + StatusIB status = 1; + } + + struct AttributeData { + optional int32u data_version = 0; + AttributePathIB path = 1; + cluster_attribute_payload data = 2; + } + + struct AttributeReportIB { + AttributeStatus attribute_status = 0; + AttributeData attribute_data = 1; + } + + struct EventStatusIB { + EventPathIB path = 0; + StatusIB status = 1; + } + + struct EventDataIB { + EventPathIB path = 0; + int64u event_number = 1; + int8u priority = 2; + + // oneof { + int64u epoch_timestamp = 3; + int64u system_timestamp = 4; + int64u delta_epoch_timestamp = 5; + int64u delta_system_timestamp = 6; + // } + + cluster_event_payload data = 2; + } + + struct EventReportIB { + EventStatusIB event_status = 0; + EventDataIB event_data = 1; + } + + struct ReportDataMessage { + optional int32u subscription_id = 0; + optional AttributeReportIB attribute_reports[] = 1; + optional EventReportIB event_reports[] = 2; + optional boolean more_cunked_messages = 3; + optional boolean suppress_response = 4; + + // 10.2.2.2. Context Tag Encoded Action Information + int8u interaction_model_revison = 0xFF; + } + + struct AttributeDataIB { + int32u data_version = 0; + AttributePathIB path = 1; + cluster_attribute_payload data = 2; + } + + struct WriteRequestMessage { + optional boolean suppres_response = 0; + boolean timed_request = 1; + AttributeDataIB write_requests[] = 2; + optional boolean more_chunked_messages = 3; + + // 10.2.2.2. Context Tag Encoded Action Information + int8u interaction_model_revison = 0xFF; + } + + struct AttributeStatusIB { + AttributePathIB path = 0; + StatusIB status = 1; + } + + struct WriteResponseMessage { + AttributeStatusIB write_responses[] = 0; + + // 10.2.2.2. Context Tag Encoded Action Information + int8u interaction_model_revison = 0xFF; + } + + struct CommandPathIB { + optional int16u endpoint_id = 0; + optional protocol_cluster_id cluster_id = 1; + optional protocol_command_id command_id = 2; + } + + struct CommandDataIB { + CommandPathIB path = 0; + cluster_command_payload data = 1; + } + + struct InvokeRequestMessage { + boolean suppress_response = 0; + boolean timed_request = 1; + CommandDataIB invoke_requests[] = 2; + + // 10.2.2.2. Context Tag Encoded Action Information + int8u interaction_model_revison = 0xFF; + } + + struct CommandStatusIB { + CommandPathIB path = 0; + StatusIB status = 1; + } + + struct InvokeResponseIB { + CommandDataIB command = 0; + CommandStatusIB status = 1; + } + + struct InvokeResponseMessage { + boolean suppress_response = 0; + InvokeResponseIB invoke_responses[] = 1; + + // 10.2.2.2. Context Tag Encoded Action Information + int8u interaction_model_revison = 0xFF; + } + + struct TimedRequestMessage { + int16u timeout = 0; + + // 10.2.2.2. Context Tag Encoded Action Information + int8u interaction_model_revison = 0xFF; + } + + // IDs here are based on Protocol opcodes, defined + // in 10.2.1 in the matter spec. + // + // Written here as "attributes" to force code generation + // to consider these active structures. + readonly attribute StatusResponseMessage status_response = 1; + readonly attribute ReadRequestMessage read_request = 2; + readonly attribute SubscribeRequestMessage subscribe_request = 3; + readonly attribute SubscribeResponseMessage subscribe_response = 4; + readonly attribute ReportDataMessage report_data = 5; + readonly attribute WriteRequestMessage write_request = 6; + readonly attribute WriteResponseMessage write_response = 7; + readonly attribute InvokeRequestMessage invoke_request = 8; + readonly attribute InvokeResponseMessage invoke_response = 9; + readonly attribute TimedRequestMessage timed_request = 10; +} + +client cluster BdxProtocol = 0xFFFF0002 { + // IDs here are based on Protocol opcodes, defined + // in 11.21.3.1 in the matter spec. + // + // Written here as "attributes" to force code generation + // to consider these active structures. + readonly attribute octet_string send_init = 1; + readonly attribute octet_string send_accept = 2; + + readonly attribute octet_string receive_init = 4; + readonly attribute octet_string receive_accept = 5; + + + readonly attribute octet_string block_query = 0x10; + readonly attribute octet_string block = 0x11; + readonly attribute octet_string block_eof = 0x12; + readonly attribute octet_string block_ack = 0x13; + readonly attribute octet_string block_ack_eof = 0x14; + readonly attribute octet_string block_query_with_skip = 0x15; +} + +client cluster UserDirectedCommissioningProtocol = 0xFFFF0003 { + struct IdentificationDeclarationStruct { + octet_string<8> instance_name = 1; + } + + // IDs here are based on Protocol opcodes, defined + // in 5.3.2 in the matter spec. + // + // Written here as "attributes" to force code generation + // to consider these active structures. + readonly attribute IdentificationDeclarationStruct identification_declaration = 0; +} \ No newline at end of file diff --git a/src/lib/format/tests/BUILD.gn b/src/lib/format/tests/BUILD.gn new file mode 100644 index 00000000000000..90428b5a575912 --- /dev/null +++ b/src/lib/format/tests/BUILD.gn @@ -0,0 +1,61 @@ +# Copyright (c) 2023 Project CHIP Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import("//build_overrides/build.gni") +import("//build_overrides/chip.gni") +import("//build_overrides/nlunit_test.gni") + +import("${chip_root}/build/chip/chip_test_suite.gni") +import("${chip_root}/build/chip/fuzz_test.gni") + +chip_test_suite("tests") { + output_name = "libFormatTests" + + test_sources = [ + "TestDecoding.cpp", + "TestFlatTree.cpp", + "TestFlatTreePosition.cpp", + ] + + sources = [ + "sample_data.cpp", + "sample_data.h", + ] + cflags = [ "-Wconversion" ] + + public_deps = [ + "${chip_root}/src/controller/data_model:cluster-tlv-metadata", + "${chip_root}/src/lib/core", + "${chip_root}/src/lib/format:flat-tree", + "${chip_root}/src/lib/format:protocol-decoder", + "${chip_root}/src/lib/format:protocol-tlv-metadata", + "${chip_root}/src/lib/support:testing", + "${nlunit_test_root}:nlunit-test", + ] +} + +if (enable_fuzz_test_targets) { + chip_fuzz_target("fuzz-payload-decoder") { + sources = [ "FuzzPayloadDecoder.cpp" ] + public_deps = [ + "${chip_root}/src/controller/data_model:cluster-tlv-metadata", + "${chip_root}/src/lib/core", + "${chip_root}/src/lib/format:flat-tree", + "${chip_root}/src/lib/format:protocol-decoder", + "${chip_root}/src/lib/format:protocol-tlv-metadata", + "${chip_root}/src/lib/support", + "${chip_root}/src/platform/logging:stdio", + ] + } +} diff --git a/src/lib/format/tests/FuzzPayloadDecoder.cpp b/src/lib/format/tests/FuzzPayloadDecoder.cpp new file mode 100644 index 00000000000000..409c4cba7173a0 --- /dev/null +++ b/src/lib/format/tests/FuzzPayloadDecoder.cpp @@ -0,0 +1,79 @@ +/* + * + * Copyright (c) 2020-2021 Project CHIP Authors + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include +#include +#include +#include + +#include +#include + +namespace { + +using namespace chip::Decoders; +using namespace chip::FlatTree; +using namespace chip::TLV; +using namespace chip::TLVMeta; + +void RunDecode(const PayloadDecoderInitParams & params, chip::ByteSpan payload) +{ + chip::Decoders::PayloadDecoder<64, 128> decoder(params); + + decoder.StartDecoding(payload); + + PayloadEntry entry; + while (decoder.Next(entry)) + { + // Nothing to do ... + } +} + +} // namespace + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t * data, size_t len) +{ + PayloadDecoderInitParams params; + params.SetProtocolDecodeTree(chip::TLVMeta::protocols_meta).SetClusterDecodeTree(chip::TLVMeta::clusters_meta); + + chip::ByteSpan payload(data, len); + + // Try all possible IM types (including an invalid one) + params.SetProtocol(chip::Protocols::InteractionModel::Id); + for (uint8_t messageType = 0; messageType < 11; messageType++) + { + RunDecode(params.SetMessageType(messageType), payload); + } + + // Try some SC variants + params.SetProtocol(chip::Protocols::SecureChannel::Id); + RunDecode(params.SetMessageType(0), payload); + RunDecode(params.SetMessageType(1), payload); + RunDecode(params.SetMessageType(2), payload); + RunDecode(params.SetMessageType(10), payload); + RunDecode(params.SetMessageType(11), payload); + RunDecode(params.SetMessageType(20), payload); + RunDecode(params.SetMessageType(32), payload); + RunDecode(params.SetMessageType(33), payload); + + params.SetProtocol(chip::Protocols::BDX::Id); + RunDecode(params.SetMessageType(1), payload); + RunDecode(params.SetMessageType(2), payload); + RunDecode(params.SetMessageType(3), payload); + + return 0; +} diff --git a/src/lib/format/tests/TestDecoding.cpp b/src/lib/format/tests/TestDecoding.cpp new file mode 100644 index 00000000000000..6fcb154de7430d --- /dev/null +++ b/src/lib/format/tests/TestDecoding.cpp @@ -0,0 +1,677 @@ +/* + * + * Copyright (c) 2023 Project CHIP Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include +#include +#include +#include + +#include +#include + +#include + +#include "sample_data.h" + +namespace { + +using namespace chip::Decoders; +using namespace chip::FlatTree; +using namespace chip::TLV; +using namespace chip::TLVMeta; +using namespace chip::TestData; + +const Entry _empty_item[0] = {}; +const std::array, 1> empty_meta = { { { 0, _empty_item } } }; + +const Entry _FakeProtocolData[] = { + { { AttributeTag(5), "proto5", ItemType::kDefault }, kInvalidNodeIndex }, + { { AttributeTag(16), "proto16", ItemType::kDefault }, kInvalidNodeIndex }, +}; + +const Entry _FakeProtocols[] = { + { { ClusterTag(0xFFFF0000), "FakeSC", ItemType::kDefault }, 1 }, + { { ClusterTag(0xFFFF0001), "FakeIM", ItemType::kDefault }, 1 }, +}; + +const std::array, 53 + 2> fake_protocols_meta = { { + { 2, _FakeProtocols }, + { 2, _FakeProtocolData }, +} }; + +void TestSampleData(nlTestSuite * inSuite, const PayloadDecoderInitParams & params, const SamplePayload & data, + const char * expectation) +{ + chip::Decoders::PayloadDecoder<64, 128> decoder( + PayloadDecoderInitParams(params).SetProtocol(data.protocolId).SetMessageType(data.messageType)); + + decoder.StartDecoding(data.payload); + + chip::StringBuilder<4096> output_builder; + + PayloadEntry entry; + int nesting = 0; + while (decoder.Next(entry)) + { + switch (entry.GetType()) + { + case PayloadEntry::IMPayloadType::kNestingExit: + nesting--; + continue; + case PayloadEntry::IMPayloadType::kAttribute: + output_builder.AddFormat("%*sATTRIBUTE: %" PRIi32 "/%" PRIi32 "\n", nesting * 2, "", entry.GetClusterId(), + entry.GetAttributeId()); + continue; + case PayloadEntry::IMPayloadType::kCommand: + output_builder.AddFormat("%*sCOMMAND: %" PRIi32 "/%" PRIi32 "\n", nesting * 2, "", entry.GetClusterId(), + entry.GetCommandId()); + continue; + case PayloadEntry::IMPayloadType::kEvent: + output_builder.AddFormat("%*sEVENT: %" PRIi32 "/%" PRIi32 "\n", nesting * 2, "", entry.GetClusterId(), + entry.GetEventId()); + continue; + default: + break; + } + + output_builder.AddFormat("%*s%s", nesting * 2, "", entry.GetName()); + + if (entry.GetType() == PayloadEntry::IMPayloadType::kNestingEnter) + { + nesting++; + } + else if (entry.GetValueText()[0] != '\0') + { + output_builder.AddFormat(": %s", entry.GetValueText()); + } + else + { + output_builder.Add(": EMPTY"); + } + output_builder.AddFormat("\n"); + } + output_builder.AddMarkerIfOverflow(); + + if (strcmp(output_builder.c_str(), expectation) != 0) + { + printf("!!!!!!!!!!!!!!!!!!! EXPECTED OUTPUT !!!!!!!!!!!!!!!!!\n"); + printf("%s\n", expectation); + printf("!!!!!!!!!!!!!!!!!!! ACTUAL OUTPUT !!!!!!!!!!!!!!!!!\n"); + printf("%s\n", output_builder.c_str()); + + unsigned idx = 0; + while (expectation[idx] == output_builder.c_str()[idx]) + { + idx++; + } + printf("!!!!!!!!!!!!!!!!!!! DIFF LOCATION !!!!!!!!!!!!!!!!!\n"); + printf("First diff at index %u\n", idx); + + chip::StringBuilder<31> partial; + printf("EXPECT: '%s'\n", partial.Reset().Add(expectation + idx).AddMarkerIfOverflow().c_str()); + printf("ACTUAL: '%s'\n", partial.Reset().Add(output_builder.c_str() + idx).AddMarkerIfOverflow().c_str()); + } + + NL_TEST_ASSERT(inSuite, strcmp(output_builder.c_str(), expectation) == 0); +} + +void TestFullDataDecoding(nlTestSuite * inSuite, void * inContext) +{ + PayloadDecoderInitParams params; + + params.SetProtocolDecodeTree(chip::TLVMeta::protocols_meta).SetClusterDecodeTree(chip::TLVMeta::clusters_meta); + + TestSampleData(inSuite, params, secure_channel_mrp_ack, "mrp_ack: EMPTY\n"); + TestSampleData(inSuite, params, secure_channel_pkbdf_param_request, + "pbkdf_param_request\n" + " initiator_random: hex:7C8698755B8E9866BB4FFDC27B733F3B6EF7F83D43FBE0CA6AD2B8C52C8F4236\n" + " initiator_session_id: 37677\n" + " passcode_id: 0\n" + " has_pbkdf_parameters: false\n"); + TestSampleData(inSuite, params, secure_channel_pkbdf_param_response, + "pbkdf_param_response\n" + " initiator_random: hex:7C8698755B8E9866BB4FFDC27B733F3B6EF7F83D43FBE0CA6AD2B8C52C8F4236\n" + " responder_random: hex:A44EB3E1A751A88A32BAB59EF16EB9764C20E1A9DDBEF6EFE3F588C943C58424\n" + " responder_session_id: 40168\n" + " pbkdf_parameters\n" + " iterations: 1000\n" + " salt: hex:E8FC1E6FD0023422B3CA7ECEDD344444551C814D3D0B0EB9C096F00E8A8051B2\n"); + TestSampleData(inSuite, params, secure_channel_pase_pake1, + // clang-format off + "pase_pake1\n" + " pA: hex:0422ABC7A84352850456BD4A510905FE6BB782A0863A9382550E1228020801B22EEC4102C60F80082842B9739705FCD37F134651442A41E3723DFFE0278\n" + // clang-format on + ); + TestSampleData(inSuite, params, secure_channel_pase_pake2, + // clang-format off + "pase_pake2\n" + " pB: hex:04B6A44A3347C6B77900A3674CA19F40F25F056F8CB344EC1B4FA7888B9E6B570B7010431C5D0BE4021FE74A96C40721765FDA6802BE8DFDF5624332275\n" + " cB: hex:40E7452275E38AEBAF0E0F6FAB33A1B0CB5AEB5E824230DD40D0071DC7E55C87\n" + // clang-format on + ); + TestSampleData(inSuite, params, secure_channel_pase_pake3, + "pase_pake3\n" + " cA: hex:6008C72EDEC9D25D4A36522F0BF23058F9378EFE38CBBCCE8C6853900169BC38\n"); + TestSampleData(inSuite, params, secure_channel_status_report, "status_report: BINARY DATA\n"); + TestSampleData(inSuite, params, im_protocol_read_request, + "read_request\n" + " attribute_requests\n" + " []\n" + " cluster_id: 49 == 'NetworkCommissioning'\n" + " attribute_id: 65532 == 'featureMap'\n" + " []\n" + " endpoint_id: 0\n" + " cluster_id: 48 == 'GeneralCommissioning'\n" + " attribute_id: 0 == 'breadcrumb'\n" + " []\n" + " endpoint_id: 0\n" + " cluster_id: 48 == 'GeneralCommissioning'\n" + " attribute_id: 1 == 'basicCommissioningInfo'\n" + " []\n" + " endpoint_id: 0\n" + " cluster_id: 48 == 'GeneralCommissioning'\n" + " attribute_id: 2 == 'regulatoryConfig'\n" + " []\n" + " endpoint_id: 0\n" + " cluster_id: 48 == 'GeneralCommissioning'\n" + " attribute_id: 3 == 'locationCapability'\n" + " []\n" + " endpoint_id: 0\n" + " cluster_id: 40 == 'BasicInformation'\n" + " attribute_id: 2 == 'vendorID'\n" + " []\n" + " endpoint_id: 0\n" + " cluster_id: 40 == 'BasicInformation'\n" + " attribute_id: 4 == 'productID'\n" + " []\n" + " cluster_id: 49 == 'NetworkCommissioning'\n" + " attribute_id: 3 == 'connectMaxTimeSeconds'\n" + " fabric_filtered: false\n" + " interaction_model_revison: 1\n"); + TestSampleData(inSuite, params, im_protocol_report_data, + "report_data\n" + " attribute_reports\n" + " []\n" + " attribute_data\n" + " data_version: 28559721\n" + " path\n" + " endpoint_id: 0\n" + " cluster_id: 49 == 'NetworkCommissioning'\n" + " attribute_id: 3 == 'connectMaxTimeSeconds'\n" + " NetworkCommissioning::connectMaxTimeSeconds: 0\n" + " []\n" + " attribute_data\n" + " data_version: 664978787\n" + " path\n" + " endpoint_id: 0\n" + " cluster_id: 40 == 'BasicInformation'\n" + " attribute_id: 4 == 'productID'\n" + " BasicInformation::productID: 32769\n" + " []\n" + " attribute_data\n" + " data_version: 664978787\n" + " path\n" + " endpoint_id: 0\n" + " cluster_id: 40 == 'BasicInformation'\n" + " attribute_id: 2 == 'vendorID'\n" + " BasicInformation::vendorID: 65521\n" + " []\n" + " attribute_data\n" + " data_version: 1414030794\n" + " path\n" + " endpoint_id: 0\n" + " cluster_id: 48 == 'GeneralCommissioning'\n" + " attribute_id: 3 == 'locationCapability'\n" + " GeneralCommissioning::locationCapability: 2 == kIndoorOutdoor\n" + " []\n" + " attribute_data\n" + " data_version: 1414030794\n" + " path\n" + " endpoint_id: 0\n" + " cluster_id: 48 == 'GeneralCommissioning'\n" + " attribute_id: 2 == 'regulatoryConfig'\n" + " GeneralCommissioning::regulatoryConfig: 0 == kIndoor\n" + " []\n" + " attribute_data\n" + " data_version: 1414030794\n" + " path\n" + " endpoint_id: 0\n" + " cluster_id: 48 == 'GeneralCommissioning'\n" + " attribute_id: 1 == 'basicCommissioningInfo'\n" + " GeneralCommissioning::basicCommissioningInfo\n" + " failSafeExpiryLengthSeconds: 60\n" + " maxCumulativeFailsafeSeconds: 900\n" + " []\n" + " attribute_data\n" + " data_version: 1414030794\n" + " path\n" + " endpoint_id: 0\n" + " cluster_id: 48 == 'GeneralCommissioning'\n" + " attribute_id: 0 == 'breadcrumb'\n" + " GeneralCommissioning::breadcrumb: 0\n" + " []\n" + " attribute_data\n" + " data_version: 28559721\n" + " path\n" + " endpoint_id: 0\n" + " cluster_id: 49 == 'NetworkCommissioning'\n" + " attribute_id: 65532 == 'featureMap'\n" + " NetworkCommissioning::featureMap: 4\n" + " suppress_response: true\n" + " interaction_model_revison: 1\n"); + + // Different content + TestSampleData(inSuite, params, im_protocol_report_data_acl, + "report_data\n" + " attribute_reports\n" + " []\n" + " attribute_data\n" + " data_version: 3420147058\n" + " path\n" + " endpoint_id: 0\n" + " cluster_id: 31 == 'AccessControl'\n" + " attribute_id: 0 == 'acl'\n" + " AccessControl::acl\n" + " []\n" + " privilege: 5 == kAdminister\n" + " authMode: 2 == kCASE\n" + " subjects\n" + " []: 112233\n" + " targets: NULL\n" + " fabricIndex: 1\n" + " suppress_response: true\n" + " interaction_model_revison: 1\n"); + + TestSampleData( + inSuite, params, im_protocol_report_data_window_covering, + "report_data\n" + " attribute_reports\n" + " []\n" + " attribute_data\n" + " data_version: 2054986218\n" + " path\n" + " endpoint_id: 1\n" + " cluster_id: 258 == 'WindowCovering'\n" + " attribute_id: 7 == 'configStatus'\n" + " WindowCovering::configStatus: 27 == kOperational | kOnlineReserved | kLiftPositionAware | kTiltPositionAware\n" + " suppress_response: true\n" + " interaction_model_revison: 1\n"); + + TestSampleData(inSuite, params, im_protocol_invoke_request, + "invoke_request\n" + " suppress_response: false\n" + " timed_request: false\n" + " invoke_requests\n" + " []\n" + " path\n" + " endpoint_id: 1\n" + " cluster_id: 6 == 'OnOff'\n" + " command_id: 2 == 'Toggle'\n" + " OnOff::Toggle\n" + " interaction_model_revison: 1\n"); + + TestSampleData(inSuite, params, im_protocol_invoke_response, + "invoke_response\n" + " suppress_response: false\n" + " invoke_responses\n" + " []\n" + " status\n" + " path\n" + " endpoint_id: 1\n" + " cluster_id: 6 == 'OnOff'\n" + " command_id: 2 == 'Toggle'\n" + " status\n" + " status: 0 == kSuccess\n" + " interaction_model_revison: 1\n"); + + TestSampleData(inSuite, params, im_protocol_invoke_request_change_channel, + "invoke_request\n" + " suppress_response: false\n" + " timed_request: false\n" + " invoke_requests\n" + " []\n" + " path\n" + " endpoint_id: 1\n" + " cluster_id: 1284 == 'Channel'\n" + " command_id: 0 == 'ChangeChannel'\n" + " Channel::ChangeChannel\n" + " match: \"channel name\"\n" + " interaction_model_revison: 1\n"); +} + +void TestMetaDataOnlyDecoding(nlTestSuite * inSuite, void * inContext) +{ + PayloadDecoderInitParams params; + + // NO CLUSTER DECODE TREE + params.SetProtocolDecodeTree(chip::TLVMeta::protocols_meta); + + TestSampleData(inSuite, params, secure_channel_mrp_ack, "mrp_ack: EMPTY\n"); + TestSampleData(inSuite, params, secure_channel_pkbdf_param_request, + "pbkdf_param_request\n" + " initiator_random: hex:7C8698755B8E9866BB4FFDC27B733F3B6EF7F83D43FBE0CA6AD2B8C52C8F4236\n" + " initiator_session_id: 37677\n" + " passcode_id: 0\n" + " has_pbkdf_parameters: false\n"); + + TestSampleData(inSuite, params, im_protocol_read_request, + "read_request\n" + " attribute_requests\n" + " []\n" + " cluster_id: 49\n" + " attribute_id: 65532\n" + " []\n" + " endpoint_id: 0\n" + " cluster_id: 48\n" + " attribute_id: 0\n" + " []\n" + " endpoint_id: 0\n" + " cluster_id: 48\n" + " attribute_id: 1\n" + " []\n" + " endpoint_id: 0\n" + " cluster_id: 48\n" + " attribute_id: 2\n" + " []\n" + " endpoint_id: 0\n" + " cluster_id: 48\n" + " attribute_id: 3\n" + " []\n" + " endpoint_id: 0\n" + " cluster_id: 40\n" + " attribute_id: 2\n" + " []\n" + " endpoint_id: 0\n" + " cluster_id: 40\n" + " attribute_id: 4\n" + " []\n" + " cluster_id: 49\n" + " attribute_id: 3\n" + " fabric_filtered: false\n" + " interaction_model_revison: 1\n"); + TestSampleData(inSuite, params, im_protocol_report_data, + "report_data\n" + " attribute_reports\n" + " []\n" + " attribute_data\n" + " data_version: 28559721\n" + " path\n" + " endpoint_id: 0\n" + " cluster_id: 49\n" + " attribute_id: 3\n" + " ATTRIBUTE: 49/3\n" + " []\n" + " attribute_data\n" + " data_version: 664978787\n" + " path\n" + " endpoint_id: 0\n" + " cluster_id: 40\n" + " attribute_id: 4\n" + " ATTRIBUTE: 40/4\n" + " []\n" + " attribute_data\n" + " data_version: 664978787\n" + " path\n" + " endpoint_id: 0\n" + " cluster_id: 40\n" + " attribute_id: 2\n" + " ATTRIBUTE: 40/2\n" + " []\n" + " attribute_data\n" + " data_version: 1414030794\n" + " path\n" + " endpoint_id: 0\n" + " cluster_id: 48\n" + " attribute_id: 3\n" + " ATTRIBUTE: 48/3\n" + " []\n" + " attribute_data\n" + " data_version: 1414030794\n" + " path\n" + " endpoint_id: 0\n" + " cluster_id: 48\n" + " attribute_id: 2\n" + " ATTRIBUTE: 48/2\n" + " []\n" + " attribute_data\n" + " data_version: 1414030794\n" + " path\n" + " endpoint_id: 0\n" + " cluster_id: 48\n" + " attribute_id: 1\n" + " ATTRIBUTE: 48/1\n" + " []\n" + " attribute_data\n" + " data_version: 1414030794\n" + " path\n" + " endpoint_id: 0\n" + " cluster_id: 48\n" + " attribute_id: 0\n" + " ATTRIBUTE: 48/0\n" + " []\n" + " attribute_data\n" + " data_version: 28559721\n" + " path\n" + " endpoint_id: 0\n" + " cluster_id: 49\n" + " attribute_id: 65532\n" + " ATTRIBUTE: 49/65532\n" + " suppress_response: true\n" + " interaction_model_revison: 1\n"); + + // Different content + TestSampleData(inSuite, params, im_protocol_report_data_acl, + "report_data\n" + " attribute_reports\n" + " []\n" + " attribute_data\n" + " data_version: 3420147058\n" + " path\n" + " endpoint_id: 0\n" + " cluster_id: 31\n" + " attribute_id: 0\n" + " ATTRIBUTE: 31/0\n" + " suppress_response: true\n" + " interaction_model_revison: 1\n"); +} + +void TestEmptyClusterMetaDataDecode(nlTestSuite * inSuite, void * inContext) +{ + PayloadDecoderInitParams params; + + params.SetProtocolDecodeTree(chip::TLVMeta::protocols_meta).SetClusterDecodeTree(empty_meta); + + TestSampleData(inSuite, params, secure_channel_mrp_ack, "mrp_ack: EMPTY\n"); + TestSampleData(inSuite, params, im_protocol_report_data_acl, + "report_data\n" + " attribute_reports\n" + " []\n" + " attribute_data\n" + " data_version: 3420147058\n" + " path\n" + " endpoint_id: 0\n" + " cluster_id: 31\n" + " attribute_id: 0\n" + " 0x1f::ATTR(0x0)\n" // Cluster 31, attribute 0 + " UnknownTag(0x100)\n" // List entry (acl is a list) + " ContextSpecific(0x1): 5\n" // privilege + " ContextSpecific(0x2): 2\n" // authMode + " ContextSpecific(0x3)\n" // subjects + " UnknownTag(0x100): 112233\n" // List entry (subjects is a list) + " ContextSpecific(0x4): NULL\n" // targets + " ContextSpecific(0xFE): 1\n" // fabricIndex + " suppress_response: true\n" + " interaction_model_revison: 1\n"); +} + +void TestMissingDecodeData(nlTestSuite * inSuite, void * inContext) +{ + PayloadDecoderInitParams params; + + params.SetProtocolDecodeTree(empty_meta).SetClusterDecodeTree(empty_meta); + + TestSampleData(inSuite, params, secure_channel_mrp_ack, "PROTO(0x0, 0x10): UNKNOWN\n"); + TestSampleData(inSuite, params, im_protocol_report_data_acl, "PROTO(0x1, 0x5): UNKNOWN\n"); +} + +void TestWrongDecodeData(nlTestSuite * inSuite, void * inContext) +{ + PayloadDecoderInitParams params; + + params.SetProtocolDecodeTree(fake_protocols_meta).SetClusterDecodeTree(empty_meta); + + TestSampleData(inSuite, params, secure_channel_mrp_ack, "proto16: EMPTY\n"); + TestSampleData(inSuite, params, im_protocol_report_data_acl, + "proto5\n" + " ContextSpecific(0x1)\n" + " UnknownTag(0x100)\n" + " ContextSpecific(0x1)\n" + " ContextSpecific(0x0): 3420147058\n" + " ContextSpecific(0x1)\n" + " ContextSpecific(0x2): 0\n" + " ContextSpecific(0x3): 31\n" + " ContextSpecific(0x4): 0\n" + " ContextSpecific(0x2)\n" + " UnknownTag(0x100)\n" + " ContextSpecific(0x1): 5\n" + " ContextSpecific(0x2): 2\n" + " ContextSpecific(0x3)\n" + " UnknownTag(0x100): 112233\n" + " ContextSpecific(0x4): NULL\n" + " ContextSpecific(0xFE): 1\n" + " ContextSpecific(0x4): true\n" + " ContextSpecific(0xFF): 1\n"); +} + +void TestNestingOverflow(nlTestSuite * inSuite, void * inContext) +{ + PayloadDecoderInitParams params; + params.SetProtocolDecodeTree(fake_protocols_meta).SetClusterDecodeTree(empty_meta); + + uint8_t data_buffer[1024]; + chip::TLV::TLVWriter writer; + + writer.Init(data_buffer, sizeof(data_buffer)); + + chip::TLV::TLVType unusedType; + + // Protocols start with an anonymous tagged structure, after which lists can be of any tags + NL_TEST_ASSERT(inSuite, writer.StartContainer(AnonymousTag(), kTLVType_Structure, unusedType) == CHIP_NO_ERROR); + + // nesting overflow here + for (uint8_t i = 0; i < 32; i++) + { + NL_TEST_ASSERT(inSuite, writer.StartContainer(ContextTag(i), kTLVType_List, unusedType) == CHIP_NO_ERROR); + } + // Go back to 24 (still too much nesting) + for (uint8_t i = 0; i < 8; i++) + { + NL_TEST_ASSERT(inSuite, writer.EndContainer(kTLVType_List) == CHIP_NO_ERROR); + } + for (uint8_t i = 0; i < 4; i++) + { + NL_TEST_ASSERT( + inSuite, writer.StartContainer(ContextTag(static_cast(i + 0x10)), kTLVType_List, unusedType) == CHIP_NO_ERROR); + } + for (uint8_t i = 0; i < 4; i++) + { + NL_TEST_ASSERT(inSuite, writer.EndContainer(kTLVType_List) == CHIP_NO_ERROR); + } + // Go back to 8 + for (uint8_t i = 0; i < 16; i++) + { + NL_TEST_ASSERT(inSuite, writer.EndContainer(kTLVType_List) == CHIP_NO_ERROR); + } + for (uint8_t i = 0; i < 4; i++) + { + NL_TEST_ASSERT( + inSuite, writer.StartContainer(ContextTag(static_cast(i + 0x20)), kTLVType_List, unusedType) == CHIP_NO_ERROR); + } + for (uint8_t i = 0; i < 4; i++) + { + NL_TEST_ASSERT(inSuite, writer.EndContainer(kTLVType_List) == CHIP_NO_ERROR); + } + // Go back to 4 + for (uint8_t i = 0; i < 4; i++) + { + NL_TEST_ASSERT(inSuite, writer.EndContainer(kTLVType_List) == CHIP_NO_ERROR); + } + for (uint8_t i = 0; i < 4; i++) + { + NL_TEST_ASSERT( + inSuite, writer.StartContainer(ContextTag(static_cast(i + 0x30)), kTLVType_List, unusedType) == CHIP_NO_ERROR); + } + for (uint8_t i = 0; i < 4; i++) + { + NL_TEST_ASSERT(inSuite, writer.EndContainer(kTLVType_List) == CHIP_NO_ERROR); + } + // close everything + for (uint8_t i = 0; i < 4; i++) + { + NL_TEST_ASSERT(inSuite, writer.EndContainer(kTLVType_List) == CHIP_NO_ERROR); + } + NL_TEST_ASSERT(inSuite, writer.EndContainer(kTLVType_Structure) == CHIP_NO_ERROR); + + SamplePayload fake_payload{ chip::Protocols::InteractionModel::Id, 5, chip::ByteSpan(data_buffer, writer.GetLengthWritten()) }; + + TestSampleData(inSuite, params, fake_payload, + "proto5\n" + " ContextSpecific(0x0)\n" + " ContextSpecific(0x1)\n" + " ContextSpecific(0x2)\n" + " ContextSpecific(0x3)\n" + " ContextSpecific(0x4)\n" + " ContextSpecific(0x5)\n" + " ContextSpecific(0x6)\n" + " ContextSpecific(0x7)\n" + " ContextSpecific(0x8)\n" + " ContextSpecific(0x9)\n" + " ContextSpecific(0xA)\n" + " ContextSpecific(0xB)\n" + " ContextSpecific(0xC)\n" + " ContextSpecific(0xD)\n" + " ContextSpecific(0xE)\n" + " ContextSpecific(0xF): NESTING DEPTH REACHED\n" + " ContextSpecific(0x20)\n" + " ContextSpecific(0x21)\n" + " ContextSpecific(0x22)\n" + " ContextSpecific(0x23)\n" + " ContextSpecific(0x30)\n" + " ContextSpecific(0x31)\n" + " ContextSpecific(0x32)\n" + " ContextSpecific(0x33)\n"); +} + +const nlTest sTests[] = { + NL_TEST_DEF("TestFullDataDecoding", TestFullDataDecoding), // + NL_TEST_DEF("TestMetaDataOnlyDecoding", TestMetaDataOnlyDecoding), // + NL_TEST_DEF("TestEmptyClusterMetaDataDecode", TestEmptyClusterMetaDataDecode), // + NL_TEST_DEF("TestMissingDecodeData", TestMissingDecodeData), // + NL_TEST_DEF("TestWrongDecodeData", TestWrongDecodeData), // + NL_TEST_DEF("TestNestingOverflow", TestNestingOverflow), // + NL_TEST_SENTINEL() // +}; + +} // namespace + +int TestDecode() +{ + nlTestSuite theSuite = { "TestDecode", sTests, nullptr, nullptr }; + nlTestRunner(&theSuite, nullptr); + return nlTestRunnerStats(&theSuite); +} + +CHIP_REGISTER_TEST_SUITE(TestDecode) diff --git a/src/lib/format/tests/TestFlatTree.cpp b/src/lib/format/tests/TestFlatTree.cpp new file mode 100644 index 00000000000000..8eebb984b6adb9 --- /dev/null +++ b/src/lib/format/tests/TestFlatTree.cpp @@ -0,0 +1,124 @@ +/* + * + * Copyright (c) 2023 Project CHIP Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include + +#include +#include + +#include + +#include + +#include + +namespace { + +using namespace chip::FlatTree; +using namespace chip::TLV; + +struct NamedTag +{ + Tag tag; + const char * name; +}; + +Entry node1[] = { + { { ContextTag(1), "hello" } }, + { { ContextTag(2), "world" } }, +}; + +Entry node2[] = { + { { ProfileTag(123, 1), "a" } }, + { { ProfileTag(234, 2), "b" } }, + { { ProfileTag(345, 3), "c" } }, +}; + +Entry node3[] = { + { { AnonymousTag(), "foo" } }, +}; + +#define _ENTRY(n) \ + { \ + sizeof(n) / sizeof(n[0]), n \ + } + +std::array, 3> tree = { { + _ENTRY(node1), + _ENTRY(node2), + _ENTRY(node3), +} }; + +class ByTag +{ +public: + constexpr ByTag(Tag tag) : mTag(tag) {} + bool operator()(const NamedTag & item) { return item.tag == mTag; } + +private: + const Tag mTag; +}; + +class ByName +{ +public: + constexpr ByName(const char * name) : mName(name) {} + bool operator()(const NamedTag & item) { return strcmp(item.name, mName) == 0; } + +private: + const char * mName; +}; + +void TestFlatTreeFind(nlTestSuite * inSuite, void * inContext) +{ + NL_TEST_ASSERT(inSuite, strcmp(FindEntry(tree, 0, ByTag(ContextTag(1)))->data.name, "hello") == 0); + NL_TEST_ASSERT(inSuite, strcmp(FindEntry(tree, 0, ByTag(ContextTag(2)))->data.name, "world") == 0); + NL_TEST_ASSERT(inSuite, FindEntry(tree, 0, ByTag(ContextTag(3))) == nullptr); + + NL_TEST_ASSERT(inSuite, FindEntry(tree, 0, ByName("hello"))->data.tag == ContextTag(1)); + NL_TEST_ASSERT(inSuite, FindEntry(tree, 0, ByName("world"))->data.tag == ContextTag(2)); + NL_TEST_ASSERT(inSuite, FindEntry(tree, 0, ByName("foo")) == nullptr); + + NL_TEST_ASSERT(inSuite, FindEntry(tree, 1, ByTag(ContextTag(1))) == nullptr); + NL_TEST_ASSERT(inSuite, strcmp(FindEntry(tree, 1, ByTag(ProfileTag(234, 2)))->data.name, "b") == 0); + NL_TEST_ASSERT(inSuite, strcmp(FindEntry(tree, 1, ByTag(ProfileTag(345, 3)))->data.name, "c") == 0); + NL_TEST_ASSERT(inSuite, FindEntry(tree, 1, ByTag(AnonymousTag())) == nullptr); + + NL_TEST_ASSERT(inSuite, FindEntry(tree, 2, ByTag(ContextTag(1))) == nullptr); + NL_TEST_ASSERT(inSuite, strcmp(FindEntry(tree, 2, ByTag(AnonymousTag()))->data.name, "foo") == 0); + + // out of array + NL_TEST_ASSERT(inSuite, FindEntry(tree, 3, ByTag(AnonymousTag())) == nullptr); + NL_TEST_ASSERT(inSuite, FindEntry(tree, 100, ByTag(AnonymousTag())) == nullptr); + NL_TEST_ASSERT(inSuite, FindEntry(tree, 1000, ByTag(AnonymousTag())) == nullptr); + NL_TEST_ASSERT(inSuite, FindEntry(tree, 9999999, ByTag(AnonymousTag())) == nullptr); +} + +const nlTest sTests[] = { + NL_TEST_DEF("TestFlatTreeFind", TestFlatTreeFind), // + NL_TEST_SENTINEL() // +}; + +} // namespace + +int TestFlatTree() +{ + nlTestSuite theSuite = { "FlatTree", sTests, nullptr, nullptr }; + nlTestRunner(&theSuite, nullptr); + return nlTestRunnerStats(&theSuite); +} + +CHIP_REGISTER_TEST_SUITE(TestFlatTree) diff --git a/src/lib/format/tests/TestFlatTreePosition.cpp b/src/lib/format/tests/TestFlatTreePosition.cpp new file mode 100644 index 00000000000000..ee5f6b3c94176d --- /dev/null +++ b/src/lib/format/tests/TestFlatTreePosition.cpp @@ -0,0 +1,274 @@ +/* + * + * Copyright (c) 2023 Project CHIP Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include +#include + +#include +#include + +#include +#include + +#include + +#include + +namespace { + +using namespace chip::FlatTree; +using namespace chip::TLV; + +struct NamedTag +{ + Tag tag; + const char * name; +}; + +/// Tree definition for +/// +/// |- hello (1) +/// \- world (2) +/// |- a (123, 1) +/// |- b (234, 2) +/// | \- foo (A) +/// \- c (345, 3) +/// +Entry node1[] = { + { { ContextTag(1), "hello" }, kInvalidNodeIndex }, + { { ContextTag(2), "world" }, 1 }, +}; + +Entry node2[] = { + { { ProfileTag(123, 1), "a" }, kInvalidNodeIndex }, + { { ProfileTag(234, 2), "b" }, 2 }, + { { ProfileTag(345, 3), "c" }, kInvalidNodeIndex }, +}; + +Entry node3[] = { + { { AnonymousTag(), "foo" }, kInvalidNodeIndex }, +}; + +#define _ENTRY(n) \ + { \ + sizeof(n) / sizeof(n[0]), n \ + } + +std::array, 3> tree = { { + _ENTRY(node1), + _ENTRY(node2), + _ENTRY(node3), +} }; + +class ByTag +{ +public: + constexpr ByTag(Tag tag) : mTag(tag) {} + bool operator()(const NamedTag & item) { return item.tag == mTag; } + +private: + const Tag mTag; +}; + +class ByName +{ +public: + constexpr ByName(const char * name) : mName(name) {} + bool operator()(const NamedTag & item) { return strcmp(item.name, mName) == 0; } + +private: + const char * mName; +}; + +#define ASSERT_HAS_NAME(p, n) \ + NL_TEST_ASSERT(inSuite, p.Get() != nullptr); \ + NL_TEST_ASSERT(inSuite, strcmp(p.Get()->name, n) == 0) + +#define ASSERT_HAS_CONTEXT_TAG(p, t) \ + NL_TEST_ASSERT(inSuite, p.Get() != nullptr); \ + NL_TEST_ASSERT(inSuite, p.Get()->tag == ContextTag(t)) + +#define ASSERT_HAS_PROFILE_TAG(p, a, b) \ + NL_TEST_ASSERT(inSuite, p.Get() != nullptr); \ + NL_TEST_ASSERT(inSuite, p.Get()->tag == ProfileTag(a, b)) + +template +std::vector GetPath(Position & position) +{ + std::vector result; + + for (const auto & item : position.CurrentPath()) + { + result.push_back(item->data.tag); + } + + return result; +} + +bool HasPath(const std::vector & v, Tag a) +{ + return (v.size() == 1) && (v[0] == a); +} + +bool HasPath(const std::vector & v, Tag a, Tag b) +{ + return (v.size() == 2) && (v[0] == a) && (v[1] == b); +} + +bool HasPath(const std::vector & v, Tag a, Tag b, Tag c) +{ + return (v.size() == 3) && (v[0] == a) && (v[1] == b) && (v[2] == c); +} + +void TestSimpleEnterExit(nlTestSuite * inSuite, void * inContext) +{ + Position position(tree.data(), tree.size()); + + // at start, top of tree has no value + NL_TEST_ASSERT(inSuite, position.Get() == nullptr); + NL_TEST_ASSERT(inSuite, GetPath(position).empty()); + + // Go to hello, try going to invalid 2x, then go back + position.Enter(ByTag(ContextTag(1))); + ASSERT_HAS_NAME(position, "hello"); + NL_TEST_ASSERT(inSuite, HasPath(GetPath(position), ContextTag(1))); + + position.Enter(ByTag(ContextTag(1))); + NL_TEST_ASSERT(inSuite, position.Get() == nullptr); + NL_TEST_ASSERT(inSuite, GetPath(position).empty()); + position.Enter(ByTag(ContextTag(1))); + NL_TEST_ASSERT(inSuite, position.Get() == nullptr); + position.Exit(); + NL_TEST_ASSERT(inSuite, position.Get() == nullptr); + position.Exit(); + NL_TEST_ASSERT(inSuite, position.Get() != nullptr); + ASSERT_HAS_NAME(position, "hello"); + NL_TEST_ASSERT(inSuite, HasPath(GetPath(position), ContextTag(1))); + position.Exit(); + NL_TEST_ASSERT(inSuite, position.Get() == nullptr); +} + +void TestDeeperEnter(nlTestSuite * inSuite, void * inContext) +{ + Position position(tree.data(), tree.size()); + + NL_TEST_ASSERT(inSuite, position.Get() == nullptr); + + position.Enter(ByName("world")); + ASSERT_HAS_CONTEXT_TAG(position, 2); + NL_TEST_ASSERT(inSuite, HasPath(GetPath(position), ContextTag(2))); + + position.Enter(ByTag(ProfileTag(123, 1))); + ASSERT_HAS_NAME(position, "a"); + NL_TEST_ASSERT(inSuite, HasPath(GetPath(position), ContextTag(2), ProfileTag(123, 1))); + + position.Enter(ByTag(AnonymousTag())); + NL_TEST_ASSERT(inSuite, position.Get() == nullptr); + NL_TEST_ASSERT(inSuite, GetPath(position).empty()); + position.Exit(); + ASSERT_HAS_NAME(position, "a"); + NL_TEST_ASSERT(inSuite, HasPath(GetPath(position), ContextTag(2), ProfileTag(123, 1))); + + position.Exit(); + ASSERT_HAS_NAME(position, "world"); + + position.Enter(ByName("b")); + ASSERT_HAS_PROFILE_TAG(position, 234, 2); + + position.Enter(ByTag(AnonymousTag())); + ASSERT_HAS_NAME(position, "foo"); + NL_TEST_ASSERT(inSuite, HasPath(GetPath(position), ContextTag(2), ProfileTag(234, 2), AnonymousTag())); + + // test some unknown + for (int i = 0; i < 100; i++) + { + position.Enter(ByTag(AnonymousTag())); + NL_TEST_ASSERT(inSuite, position.Get() == nullptr); + NL_TEST_ASSERT(inSuite, GetPath(position).empty()); + } + for (int i = 0; i < 100; i++) + { + NL_TEST_ASSERT(inSuite, position.Get() == nullptr); + NL_TEST_ASSERT(inSuite, GetPath(position).empty()); + position.Exit(); + } + NL_TEST_ASSERT(inSuite, HasPath(GetPath(position), ContextTag(2), ProfileTag(234, 2), AnonymousTag())); + ASSERT_HAS_NAME(position, "foo"); + position.Exit(); + + NL_TEST_ASSERT(inSuite, HasPath(GetPath(position), ContextTag(2), ProfileTag(234, 2))); + ASSERT_HAS_NAME(position, "b"); + position.Exit(); + + NL_TEST_ASSERT(inSuite, HasPath(GetPath(position), ContextTag(2))); + ASSERT_HAS_NAME(position, "world"); + + // root and stay there + position.Exit(); + position.Exit(); + position.Exit(); + position.Exit(); + NL_TEST_ASSERT(inSuite, GetPath(position).empty()); + + // can still navigate from the root + position.Enter(ByName("world")); + NL_TEST_ASSERT(inSuite, HasPath(GetPath(position), ContextTag(2))); + ASSERT_HAS_CONTEXT_TAG(position, 2); +} + +void TestDescendLimit(nlTestSuite * inSuite, void * inContext) +{ + Position position(tree.data(), tree.size()); + + position.Enter(ByName("world")); + ASSERT_HAS_CONTEXT_TAG(position, 2); + + position.Enter(ByName("b")); + ASSERT_HAS_PROFILE_TAG(position, 234, 2); + + // only 2 positions can be remembered. Running out of space + position.Enter(ByName("foo")); + NL_TEST_ASSERT(inSuite, position.Get() == nullptr); + NL_TEST_ASSERT(inSuite, GetPath(position).empty()); + + position.Exit(); + NL_TEST_ASSERT(inSuite, HasPath(GetPath(position), ContextTag(2), ProfileTag(234, 2))); + ASSERT_HAS_NAME(position, "b"); + ASSERT_HAS_PROFILE_TAG(position, 234, 2); + + position.Exit(); + NL_TEST_ASSERT(inSuite, HasPath(GetPath(position), ContextTag(2))); + ASSERT_HAS_NAME(position, "world"); + ASSERT_HAS_CONTEXT_TAG(position, 2); +} + +const nlTest sTests[] = { + NL_TEST_DEF("TestSimpleEnterExit", TestSimpleEnterExit), // + NL_TEST_DEF("TestDeeperEnter", TestDeeperEnter), // + NL_TEST_DEF("TestDescendLimit", TestDescendLimit), // + NL_TEST_SENTINEL() // +}; + +} // namespace + +int TestFlatTreePosition() +{ + nlTestSuite theSuite = { "FlatTree", sTests, nullptr, nullptr }; + nlTestRunner(&theSuite, nullptr); + return nlTestRunnerStats(&theSuite); +} + +CHIP_REGISTER_TEST_SUITE(TestFlatTreePosition) diff --git a/src/lib/format/tests/sample_data.cpp b/src/lib/format/tests/sample_data.cpp new file mode 100644 index 00000000000000..b5304198bdda1a --- /dev/null +++ b/src/lib/format/tests/sample_data.cpp @@ -0,0 +1,131 @@ +/* + * + * Copyright (c) 2023 Project CHIP Authors + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "sample_data.h" + +namespace chip { +namespace TestData { +namespace { + +const uint8_t payload_0_16[] = {}; + +const uint8_t payload_0_32[] = { 0x15, 0x30, 0x01, 0x20, 0X7C, 0x86, 0x98, 0x75, 0X5B, 0X8E, 0x98, 0x66, 0XBB, 0X4F, 0XFD, 0XC2, + 0X7B, 0x73, 0X3F, 0X3B, 0X6E, 0XF7, 0XF8, 0X3D, 0x43, 0XFB, 0XE0, 0XCA, 0X6A, 0XD2, 0XB8, 0XC5, + 0X2C, 0X8F, 0x42, 0x36, 0x25, 0x02, 0X2D, 0x93, 0x24, 0x03, 0x00, 0x28, 0x04, 0x18 }; + +const uint8_t payload_0_33[] = { 0x15, 0x30, 0x01, 0x20, 0X7C, 0x86, 0x98, 0x75, 0X5B, 0X8E, 0x98, 0x66, 0XBB, 0X4F, 0XFD, + 0XC2, 0X7B, 0x73, 0X3F, 0X3B, 0X6E, 0XF7, 0XF8, 0X3D, 0x43, 0XFB, 0XE0, 0XCA, 0X6A, 0XD2, + 0XB8, 0XC5, 0X2C, 0X8F, 0x42, 0x36, 0x30, 0x02, 0x20, 0XA4, 0X4E, 0XB3, 0XE1, 0XA7, 0x51, + 0XA8, 0X8A, 0x32, 0XBA, 0XB5, 0X9E, 0XF1, 0X6E, 0XB9, 0x76, 0X4C, 0x20, 0XE1, 0XA9, 0XDD, + 0XBE, 0XF6, 0XEF, 0XE3, 0XF5, 0x88, 0XC9, 0x43, 0XC5, 0x84, 0x24, 0x25, 0x03, 0XE8, 0X9C, + 0x35, 0x04, 0x25, 0x01, 0XE8, 0x03, 0x30, 0x02, 0x20, 0XE8, 0XFC, 0X1E, 0X6F, 0XD0, 0x02, + 0x34, 0x22, 0XB3, 0XCA, 0X7E, 0XCE, 0XDD, 0x34, 0x44, 0x44, 0x55, 0X1C, 0x81, 0X4D, 0X3D, + 0X0B, 0X0E, 0XB9, 0XC0, 0x96, 0XF0, 0X0E, 0X8A, 0x80, 0x51, 0XB2, 0x18, 0x18 }; + +const uint8_t payload_0_34[] = { 0x15, 0x30, 0x01, 0x41, 0x04, 0x22, 0XAB, 0XC7, 0XA8, 0x43, 0x52, 0x85, 0x04, 0x56, + 0XBD, 0X4A, 0x51, 0x09, 0x05, 0XFE, 0X6B, 0XB7, 0x82, 0XA0, 0x86, 0X3A, 0x93, 0x82, + 0x55, 0X0E, 0x12, 0x28, 0x02, 0x08, 0x01, 0XB2, 0X2E, 0XEC, 0x41, 0x02, 0XC6, 0X0F, + 0x80, 0x08, 0x28, 0x42, 0XB9, 0x73, 0x97, 0x05, 0XFC, 0XD3, 0X7F, 0x13, 0x46, 0x51, + 0x44, 0X2A, 0x41, 0XE3, 0x72, 0X3D, 0XFF, 0XE0, 0x27, 0x80, 0x77, 0x92, 0X0D, 0x18 }; + +const uint8_t payload_0_35[] = { 0x15, 0x30, 0x01, 0x41, 0x04, 0XB6, 0XA4, 0X4A, 0x33, 0x47, 0XC6, 0XB7, 0x79, 0x00, 0XA3, + 0x67, 0X4C, 0XA1, 0X9F, 0x40, 0XF2, 0X5F, 0x05, 0X6F, 0X8C, 0XB3, 0x44, 0XEC, 0X1B, 0X4F, + 0XA7, 0x88, 0X8B, 0X9E, 0X6B, 0x57, 0X0B, 0x70, 0x10, 0x43, 0X1C, 0X5D, 0X0B, 0XE4, 0x02, + 0X1F, 0XE7, 0X4A, 0x96, 0XC4, 0x07, 0x21, 0x76, 0X5F, 0XDA, 0x68, 0x02, 0XBE, 0X8D, 0XFD, + 0XF5, 0x62, 0x43, 0x32, 0x27, 0x53, 0x13, 0X4D, 0XC2, 0x30, 0x02, 0x20, 0x40, 0XE7, 0x45, + 0x22, 0x75, 0XE3, 0X8A, 0XEB, 0XAF, 0X0E, 0X0F, 0X6F, 0XAB, 0x33, 0XA1, 0XB0, 0XCB, 0X5A, + 0XEB, 0X5E, 0x82, 0x42, 0x30, 0XDD, 0x40, 0XD0, 0x07, 0X1D, 0XC7, 0XE5, 0X5C, 0x87, 0x18 }; + +const uint8_t payload_0_36[] = { 0x15, 0x30, 0x01, 0x20, 0x60, 0x08, 0XC7, 0X2E, 0XDE, 0XC9, 0XD2, 0X5D, 0X4A, + 0x36, 0x52, 0X2F, 0X0B, 0XF2, 0x30, 0x58, 0XF9, 0x37, 0X8E, 0XFE, 0x38, 0XCB, + 0XBC, 0XCE, 0X8C, 0x68, 0x53, 0x90, 0x01, 0x69, 0XBC, 0x38, 0x18 }; + +const uint8_t payload_0_64[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; + +const uint8_t payload_1_2[] = { 0x15, 0x36, 0x00, 0x17, 0x24, 0x03, 0x31, 0x25, 0x04, 0XFC, 0XFF, 0x18, 0x17, 0x24, 0x02, 0x00, + 0x24, 0x03, 0x30, 0x24, 0x04, 0x00, 0x18, 0x17, 0x24, 0x02, 0x00, 0x24, 0x03, 0x30, 0x24, 0x04, + 0x01, 0x18, 0x17, 0x24, 0x02, 0x00, 0x24, 0x03, 0x30, 0x24, 0x04, 0x02, 0x18, 0x17, 0x24, 0x02, + 0x00, 0x24, 0x03, 0x30, 0x24, 0x04, 0x03, 0x18, 0x17, 0x24, 0x02, 0x00, 0x24, 0x03, 0x28, 0x24, + 0x04, 0x02, 0x18, 0x17, 0x24, 0x02, 0x00, 0x24, 0x03, 0x28, 0x24, 0x04, 0x04, 0x18, 0x17, 0x24, + 0x03, 0x31, 0x24, 0x04, 0x03, 0x18, 0x18, 0x28, 0x03, 0x24, 0XFF, 0x01, 0x18 }; + +const uint8_t payload_1_5[] = { + 0x15, 0x36, 0x01, 0x15, 0x35, 0x01, 0x26, 0x00, 0x69, 0XC9, 0XB3, 0x01, 0x37, 0x01, 0x24, 0x02, 0x00, 0x24, 0x03, 0x31, 0x24, + 0x04, 0x03, 0x18, 0x24, 0x02, 0x00, 0x18, 0x18, 0x15, 0x35, 0x01, 0x26, 0x00, 0x63, 0XC5, 0XA2, 0x27, 0x37, 0x01, 0x24, 0x02, + 0x00, 0x24, 0x03, 0x28, 0x24, 0x04, 0x04, 0x18, 0x25, 0x02, 0x01, 0x80, 0x18, 0x18, 0x15, 0x35, 0x01, 0x26, 0x00, 0x63, 0XC5, + 0XA2, 0x27, 0x37, 0x01, 0x24, 0x02, 0x00, 0x24, 0x03, 0x28, 0x24, 0x04, 0x02, 0x18, 0x25, 0x02, 0XF1, 0XFF, 0x18, 0x18, 0x15, + 0x35, 0x01, 0x26, 0x00, 0XCA, 0x65, 0x48, 0x54, 0x37, 0x01, 0x24, 0x02, 0x00, 0x24, 0x03, 0x30, 0x24, 0x04, 0x03, 0x18, 0x24, + 0x02, 0x02, 0x18, 0x18, 0x15, 0x35, 0x01, 0x26, 0x00, 0XCA, 0x65, 0x48, 0x54, 0x37, 0x01, 0x24, 0x02, 0x00, 0x24, 0x03, 0x30, + 0x24, 0x04, 0x02, 0x18, 0x24, 0x02, 0x00, 0x18, 0x18, 0x15, 0x35, 0x01, 0x26, 0x00, 0XCA, 0x65, 0x48, 0x54, 0x37, 0x01, 0x24, + 0x02, 0x00, 0x24, 0x03, 0x30, 0x24, 0x04, 0x01, 0x18, 0x35, 0x02, 0x24, 0x00, 0X3C, 0x25, 0x01, 0x84, 0x03, 0x18, 0x18, 0x18, + 0x15, 0x35, 0x01, 0x26, 0x00, 0XCA, 0x65, 0x48, 0x54, 0x37, 0x01, 0x24, 0x02, 0x00, 0x24, 0x03, 0x30, 0x24, 0x04, 0x00, 0x18, + 0x24, 0x02, 0x00, 0x18, 0x18, 0x15, 0x35, 0x01, 0x26, 0x00, 0x69, 0XC9, 0XB3, 0x01, 0x37, 0x01, 0x24, 0x02, 0x00, 0x24, 0x03, + 0x31, 0x25, 0x04, 0XFC, 0XFF, 0x18, 0x24, 0x02, 0x04, 0x18, 0x18, 0x18, 0x29, 0x04, 0x24, 0XFF, 0x01, 0x18 +}; + +const uint8_t payload_1_5_acl[] = { 0x15, 0x36, 0x01, 0x15, 0x35, 0x01, 0x26, 0x00, 0x72, 0x4D, 0xDB, 0xCB, 0x37, 0x01, 0x24, + 0x02, 0x00, 0x24, 0x03, 0x1F, 0x24, 0x04, 0x00, 0x18, 0x36, 0x02, 0x15, 0x24, 0x01, 0x05, + 0x24, 0x02, 0x02, 0x36, 0x03, 0x06, 0x69, 0xB6, 0x01, 0x00, 0x18, 0x34, 0x04, 0x24, 0xFE, + 0x01, 0x18, 0x18, 0x18, 0x18, 0x18, 0x29, 0x04, 0x24, 0xFF, 0x01, 0x18 }; + +const uint8_t payload_1_8[] = { + 0x15, 0x28, 0x00, 0x28, 0x01, 0x36, 0x02, 0x15, 0x37, 0x00, 0x24, 0x00, 0x01, 0x24, 0x01, + 0x06, 0x24, 0x02, 0x02, 0x18, 0x35, 0x01, 0x18, 0x18, 0x18, 0x24, 0xFF, 0x01, 0x18, +}; + +const uint8_t payload_1_9[] = { + 0x15, 0x28, 0x00, 0x36, 0x01, 0x15, 0x35, 0x01, 0x37, 0x00, 0x24, 0x00, 0x01, 0x24, 0x01, 0x06, 0x24, + 0x02, 0x02, 0x18, 0x35, 0x01, 0x24, 0x00, 0x00, 0x18, 0x18, 0x18, 0x18, 0x24, 0xFF, 0x01, 0x18, +}; + +// Response for window_covering::config_status +// Response contains a bitmap value; +const uint8_t payload_1_5_window_covering[] = { 0x15, 0x36, 0x01, 0x15, 0x35, 0x01, 0x26, 0x00, 0xEA, 0x99, 0x7C, 0x7A, 0x37, + 0x01, 0x24, 0x02, 0x01, 0x25, 0x03, 0x02, 0x01, 0x24, 0x04, 0x07, 0x18, 0x24, + 0x02, 0x1B, 0x18, 0x18, 0x18, 0x29, 0x04, 0x24, 0xFF, 0x01, 0x18 }; + +// Change channel invoke. This has a command input (channel name) +const uint8_t payload_1_8_change_channel[] = { 0x15, 0x28, 0x00, 0x28, 0x01, 0x36, 0x02, 0x15, 0x37, 0x00, 0x24, 0x00, + 0x01, 0x25, 0x01, 0x04, 0x05, 0x24, 0x02, 0x00, 0x18, 0x35, 0x01, 0x2C, + 0x00, 0x0C, 0x63, 0x68, 0x61, 0x6E, 0x6E, 0x65, 0x6C, 0x20, 0x6E, 0x61, + 0x6D, 0x65, 0x18, 0x18, 0x18, 0x24, 0xFF, 0x01, 0x18 }; + +} // namespace + +const SamplePayload secure_channel_mrp_ack = { chip::Protocols::Id(VendorId::Common, 0), 16, + ByteSpan(payload_0_16, sizeof(payload_0_16)) }; +const SamplePayload secure_channel_pkbdf_param_request = { chip::Protocols::Id(VendorId::Common, 0), 32, ByteSpan(payload_0_32) }; +const SamplePayload secure_channel_pkbdf_param_response = { chip::Protocols::Id(VendorId::Common, 0), 33, ByteSpan(payload_0_33) }; +const SamplePayload secure_channel_pase_pake1 = { chip::Protocols::Id(VendorId::Common, 0), 34, ByteSpan(payload_0_34) }; +const SamplePayload secure_channel_pase_pake2 = { chip::Protocols::Id(VendorId::Common, 0), 35, ByteSpan(payload_0_35) }; +const SamplePayload secure_channel_pase_pake3 = { chip::Protocols::Id(VendorId::Common, 0), 36, ByteSpan(payload_0_36) }; +const SamplePayload secure_channel_status_report = { chip::Protocols::Id(VendorId::Common, 0), 64, ByteSpan(payload_0_64) }; + +const SamplePayload im_protocol_read_request = { chip::Protocols::Id(VendorId::Common, 1), 2, ByteSpan(payload_1_2) }; +const SamplePayload im_protocol_report_data = { chip::Protocols::Id(VendorId::Common, 1), 5, ByteSpan(payload_1_5) }; +const SamplePayload im_protocol_invoke_request = { chip::Protocols::Id(VendorId::Common, 1), 8, ByteSpan(payload_1_8) }; +const SamplePayload im_protocol_invoke_response = { chip::Protocols::Id(VendorId::Common, 1), 9, ByteSpan(payload_1_9) }; + +const SamplePayload im_protocol_invoke_request_change_channel = { chip::Protocols::Id(VendorId::Common, 1), 8, + ByteSpan(payload_1_8_change_channel) }; + +const SamplePayload im_protocol_report_data_acl = { chip::Protocols::Id(VendorId::Common, 1), 5, ByteSpan(payload_1_5_acl) }; +const SamplePayload im_protocol_report_data_window_covering = { chip::Protocols::Id(VendorId::Common, 1), 5, + ByteSpan(payload_1_5_window_covering) }; + +} // namespace TestData +} // namespace chip diff --git a/src/lib/format/tests/sample_data.h b/src/lib/format/tests/sample_data.h new file mode 100644 index 00000000000000..d8bf5864593978 --- /dev/null +++ b/src/lib/format/tests/sample_data.h @@ -0,0 +1,50 @@ +/* + * + * Copyright (c) 2023 Project CHIP Authors + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include +#include + +namespace chip { +namespace TestData { + +struct SamplePayload +{ + Protocols::Id protocolId; + uint8_t messageType; + ByteSpan payload; +}; + +extern const SamplePayload secure_channel_mrp_ack; +extern const SamplePayload secure_channel_pkbdf_param_request; +extern const SamplePayload secure_channel_pkbdf_param_response; +extern const SamplePayload secure_channel_pase_pake1; +extern const SamplePayload secure_channel_pase_pake2; +extern const SamplePayload secure_channel_pase_pake3; +extern const SamplePayload secure_channel_status_report; + +extern const SamplePayload im_protocol_read_request; +extern const SamplePayload im_protocol_report_data; +extern const SamplePayload im_protocol_invoke_request; +extern const SamplePayload im_protocol_invoke_response; + +// different data reports for content tests +extern const SamplePayload im_protocol_report_data_acl; +extern const SamplePayload im_protocol_report_data_window_covering; +extern const SamplePayload im_protocol_invoke_request_change_channel; + +} // namespace TestData +} // namespace chip diff --git a/src/lib/format/tlv_meta.h b/src/lib/format/tlv_meta.h new file mode 100644 index 00000000000000..483f2115dee78f --- /dev/null +++ b/src/lib/format/tlv_meta.h @@ -0,0 +1,84 @@ +/* + * + * Copyright (c) 2023 Project CHIP Authors + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include + +namespace chip { +namespace TLVMeta { + +static constexpr uint32_t kAttributeProfile = 1; +static constexpr uint32_t kCommandProfile = 2; +static constexpr uint32_t kEventProfile = 3; + +constexpr TLV::Tag ClusterTag(uint32_t cluster_id) +{ + return TLV::CommonTag(cluster_id); +} + +constexpr TLV::Tag AttributeTag(uint32_t attribute_id) +{ + return TLV::ProfileTag(kAttributeProfile, attribute_id); +} + +constexpr TLV::Tag CommandTag(uint32_t command_id) +{ + return TLV::ProfileTag(kCommandProfile, command_id); +} + +constexpr TLV::Tag EventTag(uint32_t event_id) +{ + return TLV::ProfileTag(kEventProfile, event_id); +} + +constexpr TLV::Tag ConstantValueTag(uint64_t value) +{ + // Re-use common tag for a constant value + // Will make "RawValue be equal to value" + return TLV::ProfileTag(static_cast(value >> 32), static_cast(value & 0xFFFFFFFF)); +} + +enum class ItemType : uint8_t +{ + kDefault, + kList, + kEnum, + kBitmap, + + // Special protocol types + kProtocolClusterId, + kProtocolAttributeId, + kProtocolCommandId, + kProtocolEventId, + + kProtocolPayloadAttribute, + kProtocolPayloadCommand, + kProtocolPayloadEvent, + + kProtocolBinaryData, +}; + +struct ItemInfo +{ + TLV::Tag tag; + const char * name; + ItemType type; +}; + +} // namespace TLVMeta +} // namespace chip diff --git a/src/lib/support/BufferWriter.h b/src/lib/support/BufferWriter.h index 72f50b327ea369..351860e2fc20b0 100644 --- a/src/lib/support/BufferWriter.h +++ b/src/lib/support/BufferWriter.h @@ -87,6 +87,28 @@ class BufferWriter uint8_t * Buffer() { return mBuf; } const uint8_t * Buffer() const { return mBuf; } + BufferWriter & Format(const char * format, ...) ENFORCE_FORMAT(2, 3) + { + va_list args; + va_start(args, format); + VFormat(format, args); + va_end(args); + return *this; + } + + void Reset() { mNeeded = 0; } + + /// Since this uses vsnprintf internally, on overflow + /// this will write one less byte that strictly can be + /// written (since null terminator will be in the binary data) + BufferWriter & VFormat(const char * format, va_list args) ENFORCE_FORMAT(2, 0); + + /// Assume a specific size for the buffer instead of mSize + /// + /// This is to allow avoiding off-by-one overflow truncation + /// when we know the underlying buffer size is larger. + BufferWriter & VFormatWithSize(size_t size, const char * format, va_list args) ENFORCE_FORMAT(3, 0); + protected: uint8_t * mBuf; size_t mSize; diff --git a/src/lib/support/StringBuilder.h b/src/lib/support/StringBuilder.h index 9de1538da1df51..16bd88d17b477a 100644 --- a/src/lib/support/StringBuilder.h +++ b/src/lib/support/StringBuilder.h @@ -63,6 +63,13 @@ class StringBuilderBase /// not fit, this replaces the last 3 characters with "." StringBuilderBase & AddMarkerIfOverflow(); + StringBuilderBase & Reset() + { + mWriter.Reset(); + NullTerminate(); + return *this; + } + /// access the underlying value const char * c_str() const { return reinterpret_cast(mWriter.Buffer()); } diff --git a/src/tracing/json/BUILD.gn b/src/tracing/json/BUILD.gn index c303db0b900a89..9f5c8f80df4bf3 100644 --- a/src/tracing/json/BUILD.gn +++ b/src/tracing/json/BUILD.gn @@ -15,6 +15,26 @@ import("//build_overrides/build.gni") import("//build_overrides/chip.gni") +import("${chip_root}/build/chip/buildconfig_header.gni") + +declare_args() { + # Include the hex content of the payload in the json output + matter_log_json_payload_hex = false + + # Include the decoded payload in the json outut + matter_log_json_payload_decode_full = false +} + +buildconfig_header("log-json-buildconfig") { + header = "log_json_build_config.h" + header_dir = "log_json" + + defines = [ + "MATTER_LOG_JSON_DECODE_HEX=${matter_log_json_payload_hex}", + "MATTER_LOG_JSON_DECODE_FULL=${matter_log_json_payload_decode_full}", + ] +} + # As this uses std::string, this library is NOT for use # for embedded devices. static_library("json") { @@ -24,9 +44,18 @@ static_library("json") { ] public_deps = [ + ":log-json-buildconfig", "${chip_root}/src/lib/address_resolve", "${chip_root}/src/tracing", "${chip_root}/src/transport", "${chip_root}/third_party/jsoncpp", ] + + if (matter_log_json_payload_decode_full) { + public_deps += [ + "${chip_root}/src/controller/data_model:cluster-tlv-metadata", + "${chip_root}/src/lib/format:protocol-decoder", + "${chip_root}/src/lib/format:protocol-tlv-metadata", + ] + } } diff --git a/src/tracing/json/json_tracing.cpp b/src/tracing/json/json_tracing.cpp index d3c3e1c54de80a..b0e3959b3922c9 100644 --- a/src/tracing/json/json_tracing.cpp +++ b/src/tracing/json/json_tracing.cpp @@ -23,6 +23,8 @@ #include #include +#include + #include #include @@ -30,6 +32,16 @@ #include #include +#if MATTER_LOG_JSON_DECODE_HEX +#include // nogncheck +#endif + +#if MATTER_LOG_JSON_DECODE_FULL +#include // nogncheck +#include // nogncheck +#include // nogncheck +#endif + namespace chip { namespace Tracing { namespace Json { @@ -38,6 +50,49 @@ namespace { using chip::StringBuilder; +#if MATTER_LOG_JSON_DECODE_FULL + +using namespace chip::Decoders; + +using PayloadDecoderType = chip::Decoders::PayloadDecoder<64, 256>; + +// Gets the current value of the decoder until a NEST exit is returned +::Json::Value GetPayload(PayloadDecoderType & decoder) +{ + ::Json::Value value; + PayloadEntry entry; + StringBuilder<128> formatter; + + while (decoder.Next(entry)) + { + switch (entry.GetType()) + { + case PayloadEntry::IMPayloadType::kNestingEnter: + formatter.Reset().Add(entry.GetName()); // name gets destroyed by decoding + value[formatter.c_str()] = GetPayload(decoder); + break; + case PayloadEntry::IMPayloadType::kNestingExit: + return value; + case PayloadEntry::IMPayloadType::kAttribute: + value[formatter.Reset().AddFormat("ATTR(%u/%u)", entry.GetClusterId(), entry.GetAttributeId()).c_str()] = + ""; + break; + case PayloadEntry::IMPayloadType::kCommand: + value[formatter.Reset().AddFormat("CMD(%u/%u)", entry.GetClusterId(), entry.GetCommandId()).c_str()] = ""; + continue; + case PayloadEntry::IMPayloadType::kEvent: + value[formatter.Reset().AddFormat("EVNT(%u/%u)", entry.GetClusterId(), entry.GetEventId()).c_str()] = ""; + continue; + default: + value[entry.GetName()] = entry.GetValueText(); + break; + } + } + return value; +} + +#endif + void DecodePayloadHeader(::Json::Value & value, const PayloadHeader * payloadHeader) { @@ -87,12 +142,30 @@ void DecodePacketHeader(::Json::Value & value, const PacketHeader * packetHeader } } -void DecodePayloadData(::Json::Value & value, chip::ByteSpan payload) +void DecodePayloadData(::Json::Value & value, chip::ByteSpan payload, Protocols::Id protocolId, uint8_t messageType) { - value["payloadSize"] = static_cast<::Json::Value::UInt>(payload.size()); + value["size"] = static_cast<::Json::Value::UInt>(payload.size()); - // TODO: a decode would be useful however it likely requires more decode - // metadata +#if MATTER_LOG_JSON_DECODE_HEX + char hex_buffer[1024]; + if (chip::Encoding::BytesToUppercaseHexString(payload.data(), payload.size(), hex_buffer, sizeof(hex_buffer)) == CHIP_NO_ERROR) + { + value["hex"] = hex_buffer; + } +#endif // MATTER_LOG_JSON_DECODE_HEX + +#if MATTER_LOG_JSON_DECODE_FULL + + PayloadDecoderType decoder(PayloadDecoderInitParams() + .SetProtocolDecodeTree(chip::TLVMeta::protocols_meta) + .SetClusterDecodeTree(chip::TLVMeta::clusters_meta) + .SetProtocol(protocolId) + .SetMessageType(messageType)); + + decoder.StartDecoding(payload); + + value["decoded"] = GetPayload(decoder); +#endif // MATTER_LOG_JSON_DECODE_FULL } } // namespace @@ -133,6 +206,7 @@ void JsonBackend::TraceInstant(const char * label, const char * group) void JsonBackend::LogMessageSend(MessageSendInfo & info) { ::Json::Value value; + value["event"] = "MessageSend"; switch (info.messageType) @@ -150,7 +224,7 @@ void JsonBackend::LogMessageSend(MessageSendInfo & info) DecodePayloadHeader(value["payloadHeader"], info.payloadHeader); DecodePacketHeader(value["packetHeader"], info.packetHeader); - DecodePayloadData(value["payload"], info.payload); + DecodePayloadData(value["payload"], info.payload, info.payloadHeader->GetProtocolID(), info.payloadHeader->GetMessageType()); OutputValue(value); } @@ -176,7 +250,7 @@ void JsonBackend::LogMessageReceived(MessageReceivedInfo & info) DecodePayloadHeader(value["payloadHeader"], info.payloadHeader); DecodePacketHeader(value["packetHeader"], info.packetHeader); - DecodePayloadData(value["payload"], info.payload); + DecodePayloadData(value["payload"], info.payload, info.payloadHeader->GetProtocolID(), info.payloadHeader->GetMessageType()); OutputValue(value); }