Decode TLV payload data and present it in a human readable format (#27638)
* Start with a flat tree library for a human TLV format
* Temp change for test
* Switch to a flat list and more flexible finding ... expect I want to find by id AND name eventually
* Clang-format
* Add tests for searching by name in a flat tree
* Provide non-array find-entry
* Have a good tree position that works for navigating and descend/ascend
* Added more documentation
* Add more unit tests
* Fix naming
* Support current path for flat tree positions
* Restyle
* Add IM message encoding, to have pretty-print of data once available
* Added secure channel message formats
* Add UDC defintions
* Rename things
* make the matter file parseable
* Attempt to start a codegen for tlv meta mapping
* Restyle
* Add missing files
* Restyle
* Have some codegen working, start defining types and names
* Start implementing a bit of a table generation. not done, but tables start to exist
* Support events for tables
* Add support for commands (untested though)
* More work, all except lists and constants are code-generated
* Restyle
* List support and better tag support including anonymous support
* Make tags specific: many tags are NOT context tags currently
* Restyle
* Add some test data for development tests
* Start adding some test support ... to be removed later
* Code compiles
* Add a unit test that compiles and runs
* Starting some decoding support. Still very much broken
* A bit more decoding, this time we handle lists. TLV interface is VERY bad
* Better decoding, we now show data
* Add some item information, to prepare for enum and bitmap decoding
* Restyle
* Add error messages on usage of command line
* remove a non yes/no argument
* Update error syntax for 2 more arguments
* update the help. using both true/false and yes/no is a mess
* Update logic for decoding
* Do not allow restyle
* Better StringBuilder formatting
* Test adjustment
* Naming update
* Restyle
* Add Format option for buffer writers and string builders
* Update comments
* Updated to only have printf inside stringbuilder and NOT bufferwriter
* Restyle
* Add missing file
* remove cpp file comments
* Fix cast to make clang happy
* Much better formatting and make the compile clang-friendly
* minor const correctness change. TLVReader has non-const getters
* Add special tags for payloads of things
* Added logic for binary data and payloads, to process things
* Start adding clusters metadata, make everything const-correct
* Minor update
* Start updating formats
* Never pass null pointer in vsnprintf, since our size available is never 0
* Restyle
* Restyle
* Better decoding
* Clean up some printfs
* Iterator decoding seems to work, including getting sub-data types
* Better organization of code ... protocols decoder is actually a class now
* Allow passing in decode trees for protocol understanding
* Better arg parsing - was able to test for invalid data
* Restyle
* Add back reset call to stringbuilder
* Restyle and make protocol decoding actually work
* Unformatted protocols/cluster meta
* Add more trace data for testing, fix SEGFAULT in decoder
* Support non-struct list entries
* Switch list decoding logic to be inside generated metadata
* Restyle
* Fix compilation and generation
* Start having codegen support for protocols metadata
* Move clusters meta to compile time codegen as well
* Restyle
* Cleanup dependencies a bit
* Start making TestDecoding be actual unit tests
* more unit tests, without protocol decoding
* Slightly better formatting
* More unit tests ... although invalid data looks odd
* Better formatting of unknown attributes
* Updated tests
* Restyle
* Better messaging, test overflows
* Removed unused file
* Undo submodule update
* Add some tests for invoke. Command list is NOT complete
* Restyle
* Yield commands that have no request structure
* Fix comment
* Start adding some unit tests for cpp-tlvmeta codegen
* Add tests for real
* Allow both hex and json at the same time for output
* Rename log_json to just json
* Add file output option for json tracing
* make the output look like a json array when outputing to file
* Restyle
* Fix support of "json:log"
* Fix support of "json:log"
* make things compile
* Rename open/close to openfile/closefile to avoid override errors
* Restyle
* StartsWith should be available now globally as it is always used
* StartsWith should be available now globally as it is always used
* Forward declare json to make arm cross compile pass
* Forward declare json to make arm cross compile pass
* Restyle
* Add some support for formatting enums and bitmaps
* Restyle
* Add json_tracing exceptions for includes checks
* Proper bitmap support with tests, status codes are bitmaps now
* Restyle
* Update test for overflow to have more unique values
* Restyle
* Fix decoding of command inputs and names
* Add a fuzz test for payload decoder
* Handle invalid TLV
* Add more error handling. Fuzzing runs longer now
* Restyle
* Make clang happy
* Fix efr32 unit test compilation
* Fix python lint
* Restyle
* Fix typo and restyle
* Restyle
* Add dependencies to flat-tree for generated code: they are needed
* make tests use uppercase for unknown tags as this is the code update I made recently
* Undo submodule update
* Make clang-tidy happy
* Fix subscribe response message indexing to match spec
---------
Co-authored-by: Andrei Litvin <andreilitvin@google.com>
diff --git a/BUILD.gn b/BUILD.gn
index 6cc0d55..5b3a9e4 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -54,6 +54,7 @@
"${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 64c0642..1a6927b 100644
--- a/build/chip/chip_codegen.gni
+++ b/build/chip/chip_codegen.gni
@@ -53,9 +53,19 @@
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 @@
"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 a6f6dce..96dd37f 100644
--- a/examples/chip-tool/args.gni
+++ b/examples/chip-tool/args.gni
@@ -27,3 +27,6 @@
# 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 53c1e65..a6f759b 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 @@
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 5d790d8..a48f187 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 @@
'--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 @@
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 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 0118c1a..1f7a7b0 100644
--- a/scripts/py_matter_idl/files.gni
+++ b/scripts/py_matter_idl/files.gni
@@ -3,12 +3,18 @@
# Templates used for generation
matter_idl_generator_templates = [
- "${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/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/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/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/java/ClusterReadMapping.jinja",
+ "${chip_root}/scripts/py_matter_idl/matter_idl/generators/java/ClusterWriteMapping.jinja",
]
matter_idl_generator_sources = [
@@ -16,11 +22,19 @@
"${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 0000000..f748875
--- /dev/null
+++ b/scripts/py_matter_idl/matter_idl/generators/cpp/tlvmeta/TLVMetaData_cpp.jinja
@@ -0,0 +1,44 @@
+#include <tlv/meta/{{table_name}}.h>
+
+namespace chip {
+namespace TLVMeta {
+namespace {
+
+using namespace chip::FlatTree;
+using namespace chip::TLV;
+
+{%- for table in sub_tables %}
+
+const Entry<ItemInfo> _{{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<ItemInfo> _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<ItemInfo> _primitive_type_list[] = {
+ { { AnonymousTag(), "[]", ItemType::kDefault }, kInvalidNodeIndex },
+};
+
+} // namespace
+
+#define _ENTRY(n) { sizeof(n) / sizeof(n[0]), n}
+
+const std::array<const Node<ItemInfo>, {{ 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 0000000..f8d1e77
--- /dev/null
+++ b/scripts/py_matter_idl/matter_idl/generators/cpp/tlvmeta/TLVMetaData_h.jinja
@@ -0,0 +1,12 @@
+#include <lib/format/tlv_meta.h>
+#include <lib/format/FlatTree.h>
+
+#include <array>
+
+namespace chip {
+namespace TLVMeta {
+
+extern const std::array<const FlatTree::Node<ItemInfo>, {{ 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 0000000..0bd57e2
--- /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 <Cluster>_<name>)
+ 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 7f1e17b..e3c8e1e 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 @@
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 @@
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 @@
'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 146b624..0576f32 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 @@
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 f8f757b..3a50ebc 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 @@
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 0000000..4985c3e
--- /dev/null
+++ b/scripts/py_matter_idl/matter_idl/tests/outputs/cluster_struct_attribute/cpp-tlvmeta/clusters_meta.cpp
@@ -0,0 +1,48 @@
+#include <tlv/meta/clusters_meta.h>
+
+namespace chip {
+namespace TLVMeta {
+namespace {
+
+using namespace chip::FlatTree;
+using namespace chip::TLV;
+
+const Entry<ItemInfo> _DemoCluster[] = {
+ { { AttributeTag(5), "singleFailSafe", ItemType::kDefault }, 3 }, // DemoCluster::ArmFailSafeRequest
+ { { AttributeTag(100), "armFailsafes", ItemType::kList }, 4 }, // DemoCluster::ArmFailSafeRequest[]
+};
+
+const Entry<ItemInfo> _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<ItemInfo> _DemoCluster_ArmFailSafeRequest_list_[] = {
+ { { AnonymousTag(), "[]", ItemType::kDefault }, 3 }, // DemoCluster_ArmFailSafeRequest[]
+};
+
+const Entry<ItemInfo> _all_clusters[] = {
+ { { ClusterTag(0x0A), "DemoCluster", ItemType::kDefault }, 2 },
+
+};
+
+// For any non-structure list like u64[] or similar.
+const Entry<ItemInfo> _primitive_type_list[] = {
+ { { AnonymousTag(), "[]", ItemType::kDefault }, kInvalidNodeIndex },
+};
+
+} // namespace
+
+#define _ENTRY(n) { sizeof(n) / sizeof(n[0]), n}
+
+const std::array<const Node<ItemInfo>, 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 0000000..c8a3aa6
--- /dev/null
+++ b/scripts/py_matter_idl/matter_idl/tests/outputs/cluster_struct_attribute/cpp-tlvmeta/clusters_meta.h
@@ -0,0 +1,12 @@
+#include <lib/format/tlv_meta.h>
+#include <lib/format/FlatTree.h>
+
+#include <array>
+
+namespace chip {
+namespace TLVMeta {
+
+extern const std::array<const FlatTree::Node<ItemInfo>, 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 0000000..05d3085
--- /dev/null
+++ b/scripts/py_matter_idl/matter_idl/tests/outputs/cluster_with_commands/cpp-tlvmeta/clusters_meta.cpp
@@ -0,0 +1,79 @@
+#include <tlv/meta/clusters_meta.h>
+
+namespace chip {
+namespace TLVMeta {
+namespace {
+
+using namespace chip::FlatTree;
+using namespace chip::TLV;
+
+const Entry<ItemInfo> _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<ItemInfo> _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<ItemInfo> _OnOff_OnOffDyingLightEffectVariant[] = {
+ { { ConstantValueTag(0x0), "k20PercenterDimUpIn0p5SecondsThenFadeToOffIn1Second", ItemType::kDefault }, kInvalidNodeIndex }, // OnOff::OnOffDyingLightEffectVariant::k20PercenterDimUpIn0p5SecondsThenFadeToOffIn1Second
+};
+
+const Entry<ItemInfo> _OnOff_OnOffEffectIdentifier[] = {
+ { { ConstantValueTag(0x0), "kDelayedAllOff", ItemType::kDefault }, kInvalidNodeIndex }, // OnOff::OnOffEffectIdentifier::kDelayedAllOff
+ { { ConstantValueTag(0x1), "kDyingLight", ItemType::kDefault }, kInvalidNodeIndex }, // OnOff::OnOffEffectIdentifier::kDyingLight
+};
+
+const Entry<ItemInfo> _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<ItemInfo> _OnOff_OnOffControl[] = {
+ { { ConstantValueTag(0x1), "kAcceptOnlyWhenOn", ItemType::kDefault }, kInvalidNodeIndex }, // OnOff::OnOffControl::kAcceptOnlyWhenOn
+};
+
+const Entry<ItemInfo> _OnOff_OnOffFeature[] = {
+ { { ConstantValueTag(0x1), "kLighting", ItemType::kDefault }, kInvalidNodeIndex }, // OnOff::OnOffFeature::kLighting
+};
+
+const Entry<ItemInfo> _OnOff_ScenesFeature[] = {
+ { { ConstantValueTag(0x1), "kSceneNames", ItemType::kDefault }, kInvalidNodeIndex }, // OnOff::ScenesFeature::kSceneNames
+};
+
+const Entry<ItemInfo> _all_clusters[] = {
+
+};
+
+// For any non-structure list like u64[] or similar.
+const Entry<ItemInfo> _primitive_type_list[] = {
+ { { AnonymousTag(), "[]", ItemType::kDefault }, kInvalidNodeIndex },
+};
+
+} // namespace
+
+#define _ENTRY(n) { sizeof(n) / sizeof(n[0]), n}
+
+const std::array<const Node<ItemInfo>, 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 0000000..f2bd083
--- /dev/null
+++ b/scripts/py_matter_idl/matter_idl/tests/outputs/cluster_with_commands/cpp-tlvmeta/clusters_meta.h
@@ -0,0 +1,12 @@
+#include <lib/format/tlv_meta.h>
+#include <lib/format/FlatTree.h>
+
+#include <array>
+
+namespace chip {
+namespace TLVMeta {
+
+extern const std::array<const FlatTree::Node<ItemInfo>, 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 ae5b29a..2a7d386 100644
--- a/scripts/py_matter_idl/setup.cfg
+++ b/scripts/py_matter_idl/setup.cfg
@@ -28,6 +28,8 @@
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 c36315f..531ee98 100644
--- a/src/BUILD.gn
+++ b/src/BUILD.gn
@@ -59,6 +59,7 @@
"${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 72e981c..0bf3c15 100644
--- a/src/controller/data_model/BUILD.gn
+++ b/src/controller/data_model/BUILD.gn
@@ -21,6 +21,25 @@
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 4355076..fd2a815 100644
--- a/src/lib/core/TLVTags.h
+++ b/src/lib/core/TLVTags.h
@@ -46,6 +46,8 @@
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 0000000..c75ecd0
--- /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 0000000..6c1eeb7
--- /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 <stddef.h>
+
+#include <array>
+#include <limits>
+
+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<size_t>::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 <typename CONTENT>
+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 <typename CONTENT>
+struct Node
+{
+ size_t entry_count; // number of items in [entries]
+ const Entry<CONTENT> * entries; // child items of [entry_count] size
+
+ /// Attempt to find the entry with given matcher.
+ ///
+ /// Returns nullptr if no matches can be found.
+ template <typename MATCHER>
+ const Entry<CONTENT> * 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 <typename CONTENT, typename MATCHER>
+inline const Entry<CONTENT> * FindEntry(const Node<CONTENT> * 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 <typename CONTENT, typename MATCHER, size_t N>
+inline const Entry<CONTENT> * FindEntry(const std::array<Node<CONTENT>, 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 0000000..e01f8b4
--- /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 <lib/format/FlatTree.h>
+#include <lib/support/Span.h>
+
+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<DataType, 10> 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 <typename CONTENT, size_t DESCEND_DEPTH>
+class Position
+{
+public:
+ Position(const Node<CONTENT> * tree, size_t treeSize) : mTree(tree), mTreeSize(treeSize) {}
+
+ template <size_t N>
+ Position(const std::array<const Node<CONTENT>, 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 <typename MATCHER>
+ 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<const Entry<CONTENT> *> CurrentPath();
+
+ bool HasValidTree() const { return mTree != nullptr; }
+
+ size_t DescendDepth() const { return mDescendDepth + mUnknownDescendDepth; }
+
+private:
+ // actual tree that we visit
+ const Node<CONTENT> * 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<CONTENT> * 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 <typename CONTENT, size_t DESCEND_DEPTH>
+const CONTENT * Position<CONTENT, DESCEND_DEPTH>::Get() const
+{
+ if (mUnknownDescendDepth > 0)
+ {
+ return nullptr;
+ }
+
+ if (mDescendDepth == 0)
+ {
+ return nullptr;
+ }
+
+ return &mPositions[mDescendDepth - 1]->data;
+}
+
+template <typename CONTENT, size_t DESCEND_DEPTH>
+template <typename MATCHER>
+void Position<CONTENT, DESCEND_DEPTH>::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<CONTENT> * child = FindEntry(mTree, mTreeSize, nodeIdx, matcher);
+
+ if (child == nullptr)
+ {
+ mUnknownDescendDepth = 1;
+ return;
+ }
+
+ mPositions[mDescendDepth++] = child;
+}
+
+template <typename CONTENT, size_t DESCEND_DEPTH>
+void Position<CONTENT, DESCEND_DEPTH>::Exit()
+{
+ if (mUnknownDescendDepth > 0)
+ {
+ mUnknownDescendDepth--;
+ return;
+ }
+
+ if (mDescendDepth > 0)
+ {
+ mDescendDepth--;
+ }
+}
+
+template <typename CONTENT, size_t DESCEND_DEPTH>
+chip::Span<const Entry<CONTENT> *> Position<CONTENT, DESCEND_DEPTH>::CurrentPath()
+{
+ if (mUnknownDescendDepth > 0)
+ {
+ return chip::Span<const Entry<CONTENT> *>();
+ }
+
+ // const chip::FlatTree::Entry<{anonymous}::NamedTag>* const* to
+ // const chip::FlatTree::Entry<{anonymous}::NamedTag>**
+ typename chip::Span<const Entry<CONTENT> *>::pointer p = mPositions;
+
+ // return chip::Span<const Entry<CONTENT> *>(mPositions, mDescendDepth);
+ return chip::Span<const Entry<CONTENT> *>(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 0000000..8c3b352
--- /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 <lib/format/protocol_decoder.h>
+
+#include <lib/format/FlatTree.h>
+#include <lib/format/FlatTreePosition.h>
+
+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<TLVTagControl>(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<int>(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 0000000..3cf3cdb
--- /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 <lib/core/TLVReader.h>
+#include <lib/format/FlatTreePosition.h>
+#include <lib/format/tlv_meta.h>
+#include <lib/support/StringBuilder.h>
+#include <protocols/Protocols.h>
+
+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<chip::TLVMeta::ItemInfo> *;
+ 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 <size_t N>
+ PayloadDecoderInitParams & SetProtocolDecodeTree(const std::array<const FlatTree::Node<chip::TLVMeta::ItemInfo>, N> & a)
+ {
+ return SetProtocolDecodeTree(a.data(), N);
+ }
+
+ PayloadDecoderInitParams & SetClusterDecodeTree(DecodeTree tree, size_t s)
+ {
+ mClusterTree = tree;
+ mClusterTreeSize = s;
+ return *this;
+ }
+
+ template <size_t N>
+ PayloadDecoderInitParams & SetClusterDecodeTree(const std::array<const FlatTree::Node<chip::TLVMeta::ItemInfo>, 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<chip::TLVMeta::ItemInfo, kMaxDecodeDepth>;
+
+ 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 <size_t kNameBufferSize, size_t kValueBufferSize>
+class PayloadDecoder : public PayloadDecoderBase
+{
+public:
+ PayloadDecoder(const PayloadDecoderInitParams & params) : PayloadDecoderBase(std::move(params), mName, mValue) {}
+
+private:
+ chip::StringBuilder<kNameBufferSize> mName;
+ chip::StringBuilder<kValueBufferSize> 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 0000000..0346b13
--- /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 0000000..90428b5
--- /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 0000000..409c4cb
--- /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 <cstddef>
+#include <cstdint>
+#include <lib/format/protocol_decoder.h>
+#include <lib/support/StringBuilder.h>
+
+#include <tlv/meta/clusters_meta.h>
+#include <tlv/meta/protocols_meta.h>
+
+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 0000000..6fcb154
--- /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 <lib/core/TLVWriter.h>
+#include <lib/format/protocol_decoder.h>
+#include <lib/support/StringBuilder.h>
+#include <lib/support/UnitTestRegistration.h>
+
+#include <tlv/meta/clusters_meta.h>
+#include <tlv/meta/protocols_meta.h>
+
+#include <nlunit-test.h>
+
+#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<ItemInfo> _empty_item[0] = {};
+const std::array<const Node<ItemInfo>, 1> empty_meta = { { { 0, _empty_item } } };
+
+const Entry<ItemInfo> _FakeProtocolData[] = {
+ { { AttributeTag(5), "proto5", ItemType::kDefault }, kInvalidNodeIndex },
+ { { AttributeTag(16), "proto16", ItemType::kDefault }, kInvalidNodeIndex },
+};
+
+const Entry<ItemInfo> _FakeProtocols[] = {
+ { { ClusterTag(0xFFFF0000), "FakeSC", ItemType::kDefault }, 1 },
+ { { ClusterTag(0xFFFF0001), "FakeIM", ItemType::kDefault }, 1 },
+};
+
+const std::array<const Node<ItemInfo>, 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<uint8_t>(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<uint8_t>(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<uint8_t>(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 0000000..8eebb98
--- /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 <lib/format/FlatTree.h>
+
+#include <lib/core/TLVTags.h>
+#include <lib/support/UnitTestRegistration.h>
+
+#include <array>
+
+#include <string.h>
+
+#include <nlunit-test.h>
+
+namespace {
+
+using namespace chip::FlatTree;
+using namespace chip::TLV;
+
+struct NamedTag
+{
+ Tag tag;
+ const char * name;
+};
+
+Entry<NamedTag> node1[] = {
+ { { ContextTag(1), "hello" } },
+ { { ContextTag(2), "world" } },
+};
+
+Entry<NamedTag> node2[] = {
+ { { ProfileTag(123, 1), "a" } },
+ { { ProfileTag(234, 2), "b" } },
+ { { ProfileTag(345, 3), "c" } },
+};
+
+Entry<NamedTag> node3[] = {
+ { { AnonymousTag(), "foo" } },
+};
+
+#define _ENTRY(n) \
+ { \
+ sizeof(n) / sizeof(n[0]), n \
+ }
+
+std::array<Node<NamedTag>, 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 0000000..ee5f6b3
--- /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 <lib/format/FlatTree.h>
+#include <lib/format/FlatTreePosition.h>
+
+#include <lib/core/TLVTags.h>
+#include <lib/support/UnitTestRegistration.h>
+
+#include <array>
+#include <vector>
+
+#include <string.h>
+
+#include <nlunit-test.h>
+
+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<NamedTag> node1[] = {
+ { { ContextTag(1), "hello" }, kInvalidNodeIndex },
+ { { ContextTag(2), "world" }, 1 },
+};
+
+Entry<NamedTag> node2[] = {
+ { { ProfileTag(123, 1), "a" }, kInvalidNodeIndex },
+ { { ProfileTag(234, 2), "b" }, 2 },
+ { { ProfileTag(345, 3), "c" }, kInvalidNodeIndex },
+};
+
+Entry<NamedTag> node3[] = {
+ { { AnonymousTag(), "foo" }, kInvalidNodeIndex },
+};
+
+#define _ENTRY(n) \
+ { \
+ sizeof(n) / sizeof(n[0]), n \
+ }
+
+std::array<Node<NamedTag>, 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 <size_t N>
+std::vector<Tag> GetPath(Position<NamedTag, N> & position)
+{
+ std::vector<Tag> result;
+
+ for (const auto & item : position.CurrentPath())
+ {
+ result.push_back(item->data.tag);
+ }
+
+ return result;
+}
+
+bool HasPath(const std::vector<Tag> & v, Tag a)
+{
+ return (v.size() == 1) && (v[0] == a);
+}
+
+bool HasPath(const std::vector<Tag> & v, Tag a, Tag b)
+{
+ return (v.size() == 2) && (v[0] == a) && (v[1] == b);
+}
+
+bool HasPath(const std::vector<Tag> & 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<NamedTag, 10> 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<NamedTag, 10> 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<NamedTag, 2> 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 0000000..b530419
--- /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 0000000..d8bf586
--- /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 <lib/support/Span.h>
+#include <protocols/Protocols.h>
+
+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 0000000..483f211
--- /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 <lib/core/TLVTags.h>
+
+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<uint32_t>(value >> 32), static_cast<uint32_t>(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 72f50b3..351860e 100644
--- a/src/lib/support/BufferWriter.h
+++ b/src/lib/support/BufferWriter.h
@@ -87,6 +87,28 @@
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 9de1538..16bd88d 100644
--- a/src/lib/support/StringBuilder.h
+++ b/src/lib/support/StringBuilder.h
@@ -63,6 +63,13 @@
/// 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<const char *>(mWriter.Buffer()); }
diff --git a/src/tracing/json/BUILD.gn b/src/tracing/json/BUILD.gn
index c303db0..9f5c8f8 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 @@
]
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 d3c3e1c..b0e3959 100644
--- a/src/tracing/json/json_tracing.cpp
+++ b/src/tracing/json/json_tracing.cpp
@@ -23,6 +23,8 @@
#include <lib/support/StringBuilder.h>
#include <transport/TracingStructs.h>
+#include <log_json/log_json_build_config.h>
+
#include <json/json.h>
#include <errno.h>
@@ -30,6 +32,16 @@
#include <sstream>
#include <string>
+#if MATTER_LOG_JSON_DECODE_HEX
+#include <lib/support/BytesToHex.h> // nogncheck
+#endif
+
+#if MATTER_LOG_JSON_DECODE_FULL
+#include <lib/format/protocol_decoder.h> // nogncheck
+#include <tlv/meta/clusters_meta.h> // nogncheck
+#include <tlv/meta/protocols_meta.h> // nogncheck
+#endif
+
namespace chip {
namespace Tracing {
namespace Json {
@@ -38,6 +50,49 @@
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()] =
+ "<NOT_DECODED>";
+ break;
+ case PayloadEntry::IMPayloadType::kCommand:
+ value[formatter.Reset().AddFormat("CMD(%u/%u)", entry.GetClusterId(), entry.GetCommandId()).c_str()] = "<NOT_DECODED>";
+ continue;
+ case PayloadEntry::IMPayloadType::kEvent:
+ value[formatter.Reset().AddFormat("EVNT(%u/%u)", entry.GetClusterId(), entry.GetEventId()).c_str()] = "<NOT_DECODED>";
+ continue;
+ default:
+ value[entry.GetName()] = entry.GetValueText();
+ break;
+ }
+ }
+ return value;
+}
+
+#endif
+
void DecodePayloadHeader(::Json::Value & value, const PayloadHeader * payloadHeader)
{
@@ -87,12 +142,30 @@
}
}
-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::LogMessageSend(MessageSendInfo & info)
{
::Json::Value value;
+
value["event"] = "MessageSend";
switch (info.messageType)
@@ -150,7 +224,7 @@
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 @@
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);
}