diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index bfc39326..cf91b3bd 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -290,6 +290,381 @@ ] } }, + "@@rules_rust+//crate_universe:extension.bzl%crate": { + "general": { + "bzlTransitiveDigest": "fAUeWtsVgP+EymOhrDf9ntuISoZseZKjJk5LTtLTQXY=", + "usagesDigest": "FcxfSc+lUnnPXa1olQPFAOA8wvEPhZ/awnNdVRFdO00=", + "recordedFileInputs": {}, + "recordedDirentsInputs": {}, + "envVariables": { + "CARGO_BAZEL_DEBUG": null, + "CARGO_BAZEL_GENERATOR_SHA256": null, + "CARGO_BAZEL_GENERATOR_URL": null, + "CARGO_BAZEL_ISOLATED": null, + "CARGO_BAZEL_REPIN": null, + "CARGO_BAZEL_REPIN_ONLY": null, + "CARGO_BAZEL_TIMEOUT": null, + "REPIN": null + }, + "generatedRepoSpecs": { + "crates": { + "repoRuleId": "@@rules_rust+//crate_universe:extensions.bzl%_generate_repo", + "attributes": { + "contents": { + "BUILD.bazel": "###############################################################################\n# @generated\n# DO NOT MODIFY: This file is auto-generated by a crate_universe tool. To \n# regenerate this file, run the following:\n#\n# bazel mod show_repo 'protobuf'\n###############################################################################\n\npackage(default_visibility = [\"//visibility:public\"])\n\nexports_files(\n [\n \"cargo-bazel.json\",\n \"crates.bzl\",\n \"defs.bzl\",\n ] + glob(\n allow_empty = True,\n include = [\"*.bazel\"],\n ),\n)\n\nfilegroup(\n name = \"srcs\",\n srcs = glob(\n allow_empty = True,\n include = [\n \"*.bazel\",\n \"*.bzl\",\n ],\n ),\n)\n\n# Workspace Member Dependencies\nalias(\n name = \"googletest-0.14.4\",\n actual = \"@crates__googletest-0.14.4//:googletest\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"googletest\",\n actual = \"@crates__googletest-0.14.4//:googletest\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"linkme-0.3.37\",\n actual = \"@crates__linkme-0.3.37//:linkme\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"linkme\",\n actual = \"@crates__linkme-0.3.37//:linkme\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"paste-1.0.15\",\n actual = \"@crates__paste-1.0.15//:paste\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"paste\",\n actual = \"@crates__paste-1.0.15//:paste\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"quote-1.0.47\",\n actual = \"@crates__quote-1.0.47//:quote\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"quote\",\n actual = \"@crates__quote-1.0.47//:quote\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"syn-2.0.119\",\n actual = \"@crates__syn-2.0.119//:syn\",\n tags = [\"manual\"],\n)\n\nalias(\n name = \"syn\",\n actual = \"@crates__syn-2.0.119//:syn\",\n tags = [\"manual\"],\n)\n", + "alias_rules.bzl": "\"\"\"Alias that transitions its target to `compilation_mode=opt`. Use `transition_alias=\"opt\"` to enable.\"\"\"\n\nload(\"@rules_cc//cc:defs.bzl\", \"CcInfo\")\nload(\"@rules_rust//rust:rust_common.bzl\", \"COMMON_PROVIDERS\")\n\ndef _transition_alias_impl(ctx):\n # `ctx.attr.actual` is a list of 1 item due to the transition\n providers = [ctx.attr.actual[0][provider] for provider in COMMON_PROVIDERS]\n if CcInfo in ctx.attr.actual[0]:\n providers.append(ctx.attr.actual[0][CcInfo])\n return providers\n\ndef _change_compilation_mode(compilation_mode):\n def _change_compilation_mode_impl(_settings, _attr):\n return {\n \"//command_line_option:compilation_mode\": compilation_mode,\n }\n\n return transition(\n implementation = _change_compilation_mode_impl,\n inputs = [],\n outputs = [\n \"//command_line_option:compilation_mode\",\n ],\n )\n\ndef _transition_alias_rule(compilation_mode):\n return rule(\n implementation = _transition_alias_impl,\n provides = COMMON_PROVIDERS,\n attrs = {\n \"actual\": attr.label(\n mandatory = True,\n doc = \"`rust_library()` target to transition to `compilation_mode=opt`.\",\n providers = COMMON_PROVIDERS,\n cfg = _change_compilation_mode(compilation_mode),\n ),\n \"_allowlist_function_transition\": attr.label(\n default = \"@bazel_tools//tools/allowlists/function_transition_allowlist\",\n ),\n },\n doc = \"Transitions a Rust library crate to the `compilation_mode=opt`.\",\n )\n\ntransition_alias_dbg = _transition_alias_rule(\"dbg\")\ntransition_alias_fastbuild = _transition_alias_rule(\"fastbuild\")\ntransition_alias_opt = _transition_alias_rule(\"opt\")\n", + "defs.bzl": "###############################################################################\n# @generated\n# DO NOT MODIFY: This file is auto-generated by a crate_universe tool. To \n# regenerate this file, run the following:\n#\n# bazel mod show_repo 'protobuf'\n###############################################################################\n\"\"\"\n# `crates_repository` API\n\n- [aliases](#aliases)\n- [crate_deps](#crate_deps)\n- [all_crate_deps](#all_crate_deps)\n- [crate_repositories](#crate_repositories)\n\n\"\"\"\n\nload(\"@bazel_tools//tools/build_defs/repo:git.bzl\", \"git_repository\")\nload(\"@bazel_tools//tools/build_defs/repo:http.bzl\", \"http_archive\")\nload(\"@bazel_tools//tools/build_defs/repo:utils.bzl\", \"maybe\")\nload(\"@bazel_skylib//lib:selects.bzl\", \"selects\")\nload(\"@rules_rust//crate_universe/private:local_crate_mirror.bzl\", \"local_crate_mirror\")\n\n###############################################################################\n# MACROS API\n###############################################################################\n\n# An identifier that represent common dependencies (unconditional).\n_COMMON_CONDITION = \"\"\n\ndef _flatten_dependency_maps(all_dependency_maps):\n \"\"\"Flatten a list of dependency maps into one dictionary.\n\n Dependency maps have the following structure:\n\n ```python\n DEPENDENCIES_MAP = {\n # The first key in the map is a Bazel package\n # name of the workspace this file is defined in.\n \"workspace_member_package\": {\n\n # Not all dependencies are supported for all platforms.\n # the condition key is the condition required to be true\n # on the host platform.\n \"condition\": {\n\n # An alias to a crate target. # The label of the crate target the\n # Aliases are only crate names. # package name refers to.\n \"package_name\": \"@full//:label\",\n }\n }\n }\n ```\n\n Args:\n all_dependency_maps (list): A list of dicts as described above\n\n Returns:\n dict: A dictionary as described above\n \"\"\"\n dependencies = {}\n\n for workspace_deps_map in all_dependency_maps:\n for pkg_name, conditional_deps_map in workspace_deps_map.items():\n if pkg_name not in dependencies:\n non_frozen_map = dict()\n for key, values in conditional_deps_map.items():\n non_frozen_map.update({key: dict(values.items())})\n dependencies.setdefault(pkg_name, non_frozen_map)\n continue\n\n for condition, deps_map in conditional_deps_map.items():\n # If the condition has not been recorded, do so and continue\n if condition not in dependencies[pkg_name]:\n dependencies[pkg_name].setdefault(condition, dict(deps_map.items()))\n continue\n\n # Alert on any miss-matched dependencies\n inconsistent_entries = []\n for crate_name, crate_label in deps_map.items():\n existing = dependencies[pkg_name][condition].get(crate_name)\n if existing and existing != crate_label:\n inconsistent_entries.append((crate_name, existing, crate_label))\n dependencies[pkg_name][condition].update({crate_name: crate_label})\n\n return dependencies\n\ndef crate_deps(deps, package_name = None):\n \"\"\"Finds the fully qualified label of the requested crates for the package where this macro is called.\n\n Args:\n deps (list): The desired list of crate targets.\n package_name (str, optional): The package name of the set of dependencies to look up.\n Defaults to `native.package_name()`.\n\n Returns:\n list: A list of labels to generated rust targets (str)\n \"\"\"\n\n if not deps:\n return []\n\n if package_name == None:\n package_name = native.package_name()\n\n # Join both sets of dependencies\n dependencies = _flatten_dependency_maps([\n _NORMAL_DEPENDENCIES,\n _NORMAL_DEV_DEPENDENCIES,\n _PROC_MACRO_DEPENDENCIES,\n _PROC_MACRO_DEV_DEPENDENCIES,\n _BUILD_DEPENDENCIES,\n _BUILD_PROC_MACRO_DEPENDENCIES,\n ]).pop(package_name, {})\n\n # Combine all conditional packages so we can easily index over a flat list\n # TODO: Perhaps this should actually return select statements and maintain\n # the conditionals of the dependencies\n flat_deps = {}\n for deps_set in dependencies.values():\n for crate_name, crate_label in deps_set.items():\n flat_deps.update({crate_name: crate_label})\n\n missing_crates = []\n crate_targets = []\n for crate_target in deps:\n if crate_target not in flat_deps:\n missing_crates.append(crate_target)\n else:\n crate_targets.append(flat_deps[crate_target])\n\n if missing_crates:\n fail(\"Could not find crates `{}` among dependencies of `{}`. Available dependencies were `{}`\".format(\n missing_crates,\n package_name,\n dependencies,\n ))\n\n return crate_targets\n\ndef all_crate_deps(\n normal = False, \n normal_dev = False, \n proc_macro = False, \n proc_macro_dev = False,\n build = False,\n build_proc_macro = False,\n package_name = None):\n \"\"\"Finds the fully qualified label of all requested direct crate dependencies \\\n for the package where this macro is called.\n\n If no parameters are set, all normal dependencies are returned. Setting any one flag will\n otherwise impact the contents of the returned list.\n\n Args:\n normal (bool, optional): If True, normal dependencies are included in the\n output list.\n normal_dev (bool, optional): If True, normal dev dependencies will be\n included in the output list.\n proc_macro (bool, optional): If True, proc_macro dependencies are included\n in the output list.\n proc_macro_dev (bool, optional): If True, dev proc_macro dependencies are\n included in the output list.\n build (bool, optional): If True, build dependencies are included\n in the output list.\n build_proc_macro (bool, optional): If True, build proc_macro dependencies are\n included in the output list.\n package_name (str, optional): The package name of the set of dependencies to look up.\n Defaults to `native.package_name()` when unset.\n\n Returns:\n list: A list of labels to generated rust targets (str)\n \"\"\"\n\n if package_name == None:\n package_name = native.package_name()\n\n # Determine the relevant maps to use\n all_dependency_maps = []\n if normal:\n all_dependency_maps.append(_NORMAL_DEPENDENCIES)\n if normal_dev:\n all_dependency_maps.append(_NORMAL_DEV_DEPENDENCIES)\n if proc_macro:\n all_dependency_maps.append(_PROC_MACRO_DEPENDENCIES)\n if proc_macro_dev:\n all_dependency_maps.append(_PROC_MACRO_DEV_DEPENDENCIES)\n if build:\n all_dependency_maps.append(_BUILD_DEPENDENCIES)\n if build_proc_macro:\n all_dependency_maps.append(_BUILD_PROC_MACRO_DEPENDENCIES)\n\n # Default to always using normal dependencies\n if not all_dependency_maps:\n all_dependency_maps.append(_NORMAL_DEPENDENCIES)\n\n dependencies = _flatten_dependency_maps(all_dependency_maps).pop(package_name, None)\n\n if not dependencies:\n if dependencies == None:\n fail(\"Tried to get all_crate_deps for package \" + package_name + \" but that package had no Cargo.toml file\")\n else:\n return []\n\n crate_deps = list(dependencies.pop(_COMMON_CONDITION, {}).values())\n for condition, deps in dependencies.items():\n crate_deps += selects.with_or({\n tuple(_CONDITIONS[condition]): deps.values(),\n \"//conditions:default\": [],\n })\n\n return crate_deps\n\ndef aliases(\n normal = False,\n normal_dev = False,\n proc_macro = False,\n proc_macro_dev = False,\n build = False,\n build_proc_macro = False,\n package_name = None):\n \"\"\"Produces a map of Crate alias names to their original label\n\n If no dependency kinds are specified, `normal` and `proc_macro` are used by default.\n Setting any one flag will otherwise determine the contents of the returned dict.\n\n Args:\n normal (bool, optional): If True, normal dependencies are included in the\n output list.\n normal_dev (bool, optional): If True, normal dev dependencies will be\n included in the output list..\n proc_macro (bool, optional): If True, proc_macro dependencies are included\n in the output list.\n proc_macro_dev (bool, optional): If True, dev proc_macro dependencies are\n included in the output list.\n build (bool, optional): If True, build dependencies are included\n in the output list.\n build_proc_macro (bool, optional): If True, build proc_macro dependencies are\n included in the output list.\n package_name (str, optional): The package name of the set of dependencies to look up.\n Defaults to `native.package_name()` when unset.\n\n Returns:\n dict: The aliases of all associated packages\n \"\"\"\n if package_name == None:\n package_name = native.package_name()\n\n # Determine the relevant maps to use\n all_aliases_maps = []\n if normal:\n all_aliases_maps.append(_NORMAL_ALIASES)\n if normal_dev:\n all_aliases_maps.append(_NORMAL_DEV_ALIASES)\n if proc_macro:\n all_aliases_maps.append(_PROC_MACRO_ALIASES)\n if proc_macro_dev:\n all_aliases_maps.append(_PROC_MACRO_DEV_ALIASES)\n if build:\n all_aliases_maps.append(_BUILD_ALIASES)\n if build_proc_macro:\n all_aliases_maps.append(_BUILD_PROC_MACRO_ALIASES)\n\n # Default to always using normal aliases\n if not all_aliases_maps:\n all_aliases_maps.append(_NORMAL_ALIASES)\n all_aliases_maps.append(_PROC_MACRO_ALIASES)\n\n aliases = _flatten_dependency_maps(all_aliases_maps).pop(package_name, None)\n\n if not aliases:\n return dict()\n\n common_items = aliases.pop(_COMMON_CONDITION, {}).items()\n\n # If there are only common items in the dictionary, immediately return them\n if not len(aliases.keys()) == 1:\n return dict(common_items)\n\n # Build a single select statement where each conditional has accounted for the\n # common set of aliases.\n crate_aliases = {\"//conditions:default\": dict(common_items)}\n for condition, deps in aliases.items():\n condition_triples = _CONDITIONS[condition]\n for triple in condition_triples:\n if triple in crate_aliases:\n crate_aliases[triple].update(deps)\n else:\n crate_aliases.update({triple: dict(deps.items() + common_items)})\n\n return select(crate_aliases)\n\n###############################################################################\n# WORKSPACE MEMBER DEPS AND ALIASES\n###############################################################################\n\n_NORMAL_DEPENDENCIES = {\n \"\": {\n _COMMON_CONDITION: {\n \"googletest\": Label(\"@crates//:googletest-0.14.4\"),\n \"linkme\": Label(\"@crates//:linkme-0.3.37\"),\n \"quote\": Label(\"@crates//:quote-1.0.47\"),\n \"syn\": Label(\"@crates//:syn-2.0.119\"),\n },\n },\n}\n\n\n_NORMAL_ALIASES = {\n \"\": {\n _COMMON_CONDITION: {\n },\n },\n}\n\n\n_NORMAL_DEV_DEPENDENCIES = {\n \"\": {\n },\n}\n\n\n_NORMAL_DEV_ALIASES = {\n \"\": {\n },\n}\n\n\n_PROC_MACRO_DEPENDENCIES = {\n \"\": {\n _COMMON_CONDITION: {\n \"paste\": Label(\"@crates//:paste-1.0.15\"),\n },\n },\n}\n\n\n_PROC_MACRO_ALIASES = {\n \"\": {\n },\n}\n\n\n_PROC_MACRO_DEV_DEPENDENCIES = {\n \"\": {\n },\n}\n\n\n_PROC_MACRO_DEV_ALIASES = {\n \"\": {\n },\n}\n\n\n_BUILD_DEPENDENCIES = {\n \"\": {\n },\n}\n\n\n_BUILD_ALIASES = {\n \"\": {\n },\n}\n\n\n_BUILD_PROC_MACRO_DEPENDENCIES = {\n \"\": {\n },\n}\n\n\n_BUILD_PROC_MACRO_ALIASES = {\n \"\": {\n },\n}\n\n\n_CONDITIONS = {\n \"aarch64-apple-darwin\": [\"@rules_rust//rust/platform:aarch64-apple-darwin\"],\n \"aarch64-unknown-linux-gnu\": [\"@rules_rust//rust/platform:aarch64-unknown-linux-gnu\"],\n \"wasm32-unknown-unknown\": [\"@rules_rust//rust/platform:wasm32-unknown-unknown\"],\n \"wasm32-wasip1\": [\"@rules_rust//rust/platform:wasm32-wasip1\"],\n \"x86_64-pc-windows-msvc\": [\"@rules_rust//rust/platform:x86_64-pc-windows-msvc\"],\n \"x86_64-unknown-linux-gnu\": [\"@rules_rust//rust/platform:x86_64-unknown-linux-gnu\"],\n \"x86_64-unknown-nixos-gnu\": [\"@rules_rust//rust/platform:x86_64-unknown-nixos-gnu\"],\n}\n\n###############################################################################\n\ndef crate_repositories():\n \"\"\"A macro for defining repositories for all generated crates.\n\n Returns:\n A list of repos visible to the module through the module extension.\n \"\"\"\n maybe(\n http_archive,\n name = \"crates__aho-corasick-1.1.5\",\n sha256 = \"c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/aho-corasick/1.1.5/download\"],\n strip_prefix = \"aho-corasick-1.1.5\",\n build_file = Label(\"@crates//crates:BUILD.aho-corasick-1.1.5.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"crates__autocfg-1.5.1\",\n sha256 = \"f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/autocfg/1.5.1/download\"],\n strip_prefix = \"autocfg-1.5.1\",\n build_file = Label(\"@crates//crates:BUILD.autocfg-1.5.1.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"crates__googletest-0.14.4\",\n sha256 = \"07de445a934242060307c2570b9f2dc4788b02ff3637de2f42268756a3eaad4c\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/googletest/0.14.4/download\"],\n strip_prefix = \"googletest-0.14.4\",\n build_file = Label(\"@crates//crates:BUILD.googletest-0.14.4.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"crates__googletest_macro-0.14.4\",\n sha256 = \"088fcbe402e99d60ed6611e1b5105ab556b8a984a6a420977850d806c2aba882\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/googletest_macro/0.14.4/download\"],\n strip_prefix = \"googletest_macro-0.14.4\",\n build_file = Label(\"@crates//crates:BUILD.googletest_macro-0.14.4.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"crates__linkme-0.3.37\",\n sha256 = \"3045e122bd98aef8ec3ad58ce84f0791f64e70163d1a02710af4aa11a4d54cc5\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/linkme/0.3.37/download\"],\n strip_prefix = \"linkme-0.3.37\",\n build_file = Label(\"@crates//crates:BUILD.linkme-0.3.37.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"crates__linkme-impl-0.3.37\",\n sha256 = \"77060ebe535362c3da75682cd17b0431017b6e7c5661e714fc69a7ad017d1301\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/linkme-impl/0.3.37/download\"],\n strip_prefix = \"linkme-impl-0.3.37\",\n build_file = Label(\"@crates//crates:BUILD.linkme-impl-0.3.37.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"crates__memchr-2.8.3\",\n sha256 = \"cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/memchr/2.8.3/download\"],\n strip_prefix = \"memchr-2.8.3\",\n build_file = Label(\"@crates//crates:BUILD.memchr-2.8.3.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"crates__num-traits-0.2.19\",\n sha256 = \"071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/num-traits/0.2.19/download\"],\n strip_prefix = \"num-traits-0.2.19\",\n build_file = Label(\"@crates//crates:BUILD.num-traits-0.2.19.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"crates__paste-1.0.15\",\n sha256 = \"57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/paste/1.0.15/download\"],\n strip_prefix = \"paste-1.0.15\",\n build_file = Label(\"@crates//crates:BUILD.paste-1.0.15.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"crates__proc-macro2-1.0.107\",\n sha256 = \"985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/proc-macro2/1.0.107/download\"],\n strip_prefix = \"proc-macro2-1.0.107\",\n build_file = Label(\"@crates//crates:BUILD.proc-macro2-1.0.107.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"crates__quote-1.0.47\",\n sha256 = \"1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/quote/1.0.47/download\"],\n strip_prefix = \"quote-1.0.47\",\n build_file = Label(\"@crates//crates:BUILD.quote-1.0.47.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"crates__regex-1.13.1\",\n sha256 = \"f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/regex/1.13.1/download\"],\n strip_prefix = \"regex-1.13.1\",\n build_file = Label(\"@crates//crates:BUILD.regex-1.13.1.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"crates__regex-automata-0.4.18\",\n sha256 = \"ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/regex-automata/0.4.18/download\"],\n strip_prefix = \"regex-automata-0.4.18\",\n build_file = Label(\"@crates//crates:BUILD.regex-automata-0.4.18.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"crates__regex-syntax-0.8.11\",\n sha256 = \"d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/regex-syntax/0.8.11/download\"],\n strip_prefix = \"regex-syntax-0.8.11\",\n build_file = Label(\"@crates//crates:BUILD.regex-syntax-0.8.11.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"crates__rustversion-1.0.23\",\n sha256 = \"cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/rustversion/1.0.23/download\"],\n strip_prefix = \"rustversion-1.0.23\",\n build_file = Label(\"@crates//crates:BUILD.rustversion-1.0.23.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"crates__syn-2.0.119\",\n sha256 = \"872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/syn/2.0.119/download\"],\n strip_prefix = \"syn-2.0.119\",\n build_file = Label(\"@crates//crates:BUILD.syn-2.0.119.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"crates__syn-3.0.9\",\n sha256 = \"d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/syn/3.0.9/download\"],\n strip_prefix = \"syn-3.0.9\",\n build_file = Label(\"@crates//crates:BUILD.syn-3.0.9.bazel\"),\n )\n\n maybe(\n http_archive,\n name = \"crates__unicode-ident-1.0.27\",\n sha256 = \"a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563\",\n type = \"tar.gz\",\n urls = [\"https://static.crates.io/crates/unicode-ident/1.0.27/download\"],\n strip_prefix = \"unicode-ident-1.0.27\",\n build_file = Label(\"@crates//crates:BUILD.unicode-ident-1.0.27.bazel\"),\n )\n\n return [\n struct(repo=\"crates__googletest-0.14.4\", is_dev_dep = False),\n struct(repo=\"crates__linkme-0.3.37\", is_dev_dep = False),\n struct(repo=\"crates__paste-1.0.15\", is_dev_dep = False),\n struct(repo=\"crates__quote-1.0.47\", is_dev_dep = False),\n struct(repo=\"crates__syn-2.0.119\", is_dev_dep = False),\n ]\n" + } + } + }, + "crates__aho-corasick-1.1.5": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "patch_args": [], + "patch_tool": "", + "patches": [], + "remote_patch_strip": 1, + "sha256": "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba", + "type": "tar.gz", + "urls": [ + "https://static.crates.io/crates/aho-corasick/1.1.5/download" + ], + "strip_prefix": "aho-corasick-1.1.5", + "build_file_content": "###############################################################################\n# @generated\n# DO NOT MODIFY: This file is auto-generated by a crate_universe tool. To \n# regenerate this file, run the following:\n#\n# bazel mod show_repo 'protobuf'\n###############################################################################\n\nload(\"@rules_rust//cargo:defs.bzl\", \"cargo_toml_env_vars\")\n\nload(\"@rules_rust//rust:defs.bzl\", \"rust_library\")\n\n# buildifier: disable=bzl-visibility\nload(\"@rules_rust//crate_universe/private:selects.bzl\", \"selects\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\ncargo_toml_env_vars(\n name = \"cargo_toml_env_vars\",\n src = \"Cargo.toml\",\n)\n\nrust_library(\n name = \"aho_corasick\",\n deps = [\n \"@crates__memchr-2.8.3//:memchr\",\n ],\n compile_data = glob(\n allow_empty = True,\n include = [\"**\"],\n exclude = [\n \"**/* *\",\n \".tmp_git_root/**/*\",\n \"BUILD\",\n \"BUILD.bazel\",\n \"WORKSPACE\",\n \"WORKSPACE.bazel\",\n ],\n ),\n crate_features = [\n \"perf-literal\",\n \"std\",\n ],\n crate_root = \"src/lib.rs\",\n edition = \"2021\",\n rustc_env_files = [\n \":cargo_toml_env_vars\",\n ],\n rustc_flags = [\n \"--cap-lints=allow\",\n ],\n srcs = glob(\n allow_empty = True,\n include = [\"**/*.rs\"],\n ),\n tags = [\n \"cargo-bazel\",\n \"crate-name=aho-corasick\",\n \"manual\",\n \"noclippy\",\n \"norustfmt\",\n ],\n target_compatible_with = select({\n \"@rules_rust//rust/platform:aarch64-apple-darwin\": [],\n \"@rules_rust//rust/platform:aarch64-unknown-linux-gnu\": [],\n \"@rules_rust//rust/platform:wasm32-unknown-unknown\": [],\n \"@rules_rust//rust/platform:wasm32-wasip1\": [],\n \"@rules_rust//rust/platform:x86_64-pc-windows-msvc\": [],\n \"@rules_rust//rust/platform:x86_64-unknown-linux-gnu\": [],\n \"@rules_rust//rust/platform:x86_64-unknown-nixos-gnu\": [],\n \"//conditions:default\": [\"@platforms//:incompatible\"],\n }),\n version = \"1.1.5\",\n)\n" + } + }, + "crates__autocfg-1.5.1": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "patch_args": [], + "patch_tool": "", + "patches": [], + "remote_patch_strip": 1, + "sha256": "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53", + "type": "tar.gz", + "urls": [ + "https://static.crates.io/crates/autocfg/1.5.1/download" + ], + "strip_prefix": "autocfg-1.5.1", + "build_file_content": "###############################################################################\n# @generated\n# DO NOT MODIFY: This file is auto-generated by a crate_universe tool. To \n# regenerate this file, run the following:\n#\n# bazel mod show_repo 'protobuf'\n###############################################################################\n\nload(\"@rules_rust//cargo:defs.bzl\", \"cargo_toml_env_vars\")\n\nload(\"@rules_rust//rust:defs.bzl\", \"rust_library\")\n\n# buildifier: disable=bzl-visibility\nload(\"@rules_rust//crate_universe/private:selects.bzl\", \"selects\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\ncargo_toml_env_vars(\n name = \"cargo_toml_env_vars\",\n src = \"Cargo.toml\",\n)\n\nrust_library(\n name = \"autocfg\",\n compile_data = glob(\n allow_empty = True,\n include = [\"**\"],\n exclude = [\n \"**/* *\",\n \".tmp_git_root/**/*\",\n \"BUILD\",\n \"BUILD.bazel\",\n \"WORKSPACE\",\n \"WORKSPACE.bazel\",\n ],\n ),\n crate_root = \"src/lib.rs\",\n edition = \"2015\",\n rustc_env_files = [\n \":cargo_toml_env_vars\",\n ],\n rustc_flags = [\n \"--cap-lints=allow\",\n ],\n srcs = glob(\n allow_empty = True,\n include = [\"**/*.rs\"],\n ),\n tags = [\n \"cargo-bazel\",\n \"crate-name=autocfg\",\n \"manual\",\n \"noclippy\",\n \"norustfmt\",\n ],\n target_compatible_with = select({\n \"@rules_rust//rust/platform:aarch64-apple-darwin\": [],\n \"@rules_rust//rust/platform:aarch64-unknown-linux-gnu\": [],\n \"@rules_rust//rust/platform:wasm32-unknown-unknown\": [],\n \"@rules_rust//rust/platform:wasm32-wasip1\": [],\n \"@rules_rust//rust/platform:x86_64-pc-windows-msvc\": [],\n \"@rules_rust//rust/platform:x86_64-unknown-linux-gnu\": [],\n \"@rules_rust//rust/platform:x86_64-unknown-nixos-gnu\": [],\n \"//conditions:default\": [\"@platforms//:incompatible\"],\n }),\n version = \"1.5.1\",\n)\n" + } + }, + "crates__googletest-0.14.4": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "patch_args": [], + "patch_tool": "", + "patches": [], + "remote_patch_strip": 1, + "sha256": "07de445a934242060307c2570b9f2dc4788b02ff3637de2f42268756a3eaad4c", + "type": "tar.gz", + "urls": [ + "https://static.crates.io/crates/googletest/0.14.4/download" + ], + "strip_prefix": "googletest-0.14.4", + "build_file_content": "###############################################################################\n# @generated\n# DO NOT MODIFY: This file is auto-generated by a crate_universe tool. To \n# regenerate this file, run the following:\n#\n# bazel mod show_repo 'protobuf'\n###############################################################################\n\nload(\"@rules_rust//cargo:defs.bzl\", \"cargo_toml_env_vars\")\n\nload(\"@rules_rust//rust:defs.bzl\", \"rust_library\")\n\n# buildifier: disable=bzl-visibility\nload(\"@rules_rust//crate_universe/private:selects.bzl\", \"selects\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\ncargo_toml_env_vars(\n name = \"cargo_toml_env_vars\",\n src = \"Cargo.toml\",\n)\n\nrust_library(\n name = \"googletest\",\n deps = [\n \"@crates__num-traits-0.2.19//:num_traits\",\n \"@crates__regex-1.13.1//:regex\",\n ],\n proc_macro_deps = [\n \"@crates__googletest_macro-0.14.4//:googletest_macro\",\n \"@crates__rustversion-1.0.23//:rustversion\",\n ],\n compile_data = glob(\n allow_empty = True,\n include = [\"**\"],\n exclude = [\n \"**/* *\",\n \".tmp_git_root/**/*\",\n \"BUILD\",\n \"BUILD.bazel\",\n \"WORKSPACE\",\n \"WORKSPACE.bazel\",\n ],\n ),\n crate_root = \"src/lib.rs\",\n edition = \"2021\",\n rustc_env_files = [\n \":cargo_toml_env_vars\",\n ],\n rustc_flags = [\n \"--cap-lints=allow\",\n ],\n srcs = glob(\n allow_empty = True,\n include = [\"**/*.rs\"],\n ),\n tags = [\n \"cargo-bazel\",\n \"crate-name=googletest\",\n \"manual\",\n \"noclippy\",\n \"norustfmt\",\n ],\n target_compatible_with = select({\n \"@rules_rust//rust/platform:aarch64-apple-darwin\": [],\n \"@rules_rust//rust/platform:aarch64-unknown-linux-gnu\": [],\n \"@rules_rust//rust/platform:wasm32-unknown-unknown\": [],\n \"@rules_rust//rust/platform:wasm32-wasip1\": [],\n \"@rules_rust//rust/platform:x86_64-pc-windows-msvc\": [],\n \"@rules_rust//rust/platform:x86_64-unknown-linux-gnu\": [],\n \"@rules_rust//rust/platform:x86_64-unknown-nixos-gnu\": [],\n \"//conditions:default\": [\"@platforms//:incompatible\"],\n }),\n version = \"0.14.4\",\n)\n" + } + }, + "crates__googletest_macro-0.14.4": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "patch_args": [], + "patch_tool": "", + "patches": [], + "remote_patch_strip": 1, + "sha256": "088fcbe402e99d60ed6611e1b5105ab556b8a984a6a420977850d806c2aba882", + "type": "tar.gz", + "urls": [ + "https://static.crates.io/crates/googletest_macro/0.14.4/download" + ], + "strip_prefix": "googletest_macro-0.14.4", + "build_file_content": "###############################################################################\n# @generated\n# DO NOT MODIFY: This file is auto-generated by a crate_universe tool. To \n# regenerate this file, run the following:\n#\n# bazel mod show_repo 'protobuf'\n###############################################################################\n\nload(\"@rules_rust//cargo:defs.bzl\", \"cargo_toml_env_vars\")\n\nload(\"@rules_rust//rust:defs.bzl\", \"rust_proc_macro\")\n\n# buildifier: disable=bzl-visibility\nload(\"@rules_rust//crate_universe/private:selects.bzl\", \"selects\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\ncargo_toml_env_vars(\n name = \"cargo_toml_env_vars\",\n src = \"Cargo.toml\",\n)\n\nrust_proc_macro(\n name = \"googletest_macro\",\n deps = [\n \"@crates__proc-macro2-1.0.107//:proc_macro2\",\n \"@crates__quote-1.0.47//:quote\",\n \"@crates__syn-2.0.119//:syn\",\n ],\n compile_data = glob(\n allow_empty = True,\n include = [\"**\"],\n exclude = [\n \"**/* *\",\n \".tmp_git_root/**/*\",\n \"BUILD\",\n \"BUILD.bazel\",\n \"WORKSPACE\",\n \"WORKSPACE.bazel\",\n ],\n ),\n crate_root = \"src/lib.rs\",\n edition = \"2021\",\n rustc_env_files = [\n \":cargo_toml_env_vars\",\n ],\n rustc_flags = [\n \"--cap-lints=allow\",\n ],\n srcs = glob(\n allow_empty = True,\n include = [\"**/*.rs\"],\n ),\n tags = [\n \"cargo-bazel\",\n \"crate-name=googletest_macro\",\n \"manual\",\n \"noclippy\",\n \"norustfmt\",\n ],\n target_compatible_with = select({\n \"@rules_rust//rust/platform:aarch64-apple-darwin\": [],\n \"@rules_rust//rust/platform:aarch64-unknown-linux-gnu\": [],\n \"@rules_rust//rust/platform:wasm32-unknown-unknown\": [],\n \"@rules_rust//rust/platform:wasm32-wasip1\": [],\n \"@rules_rust//rust/platform:x86_64-pc-windows-msvc\": [],\n \"@rules_rust//rust/platform:x86_64-unknown-linux-gnu\": [],\n \"@rules_rust//rust/platform:x86_64-unknown-nixos-gnu\": [],\n \"//conditions:default\": [\"@platforms//:incompatible\"],\n }),\n version = \"0.14.4\",\n)\n" + } + }, + "crates__linkme-0.3.37": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "patch_args": [], + "patch_tool": "", + "patches": [], + "remote_patch_strip": 1, + "sha256": "3045e122bd98aef8ec3ad58ce84f0791f64e70163d1a02710af4aa11a4d54cc5", + "type": "tar.gz", + "urls": [ + "https://static.crates.io/crates/linkme/0.3.37/download" + ], + "strip_prefix": "linkme-0.3.37", + "build_file_content": "###############################################################################\n# @generated\n# DO NOT MODIFY: This file is auto-generated by a crate_universe tool. To \n# regenerate this file, run the following:\n#\n# bazel mod show_repo 'protobuf'\n###############################################################################\n\nload(\n \"@rules_rust//cargo:defs.bzl\",\n \"cargo_build_script\",\n \"cargo_toml_env_vars\",\n)\n\nload(\"@rules_rust//rust:defs.bzl\", \"rust_library\")\n\n# buildifier: disable=bzl-visibility\nload(\"@rules_rust//crate_universe/private:selects.bzl\", \"selects\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\ncargo_toml_env_vars(\n name = \"cargo_toml_env_vars\",\n src = \"Cargo.toml\",\n)\n\nrust_library(\n name = \"linkme\",\n deps = [\n \"@crates__linkme-0.3.37//:build_script_build\",\n ],\n proc_macro_deps = [\n \"@crates__linkme-impl-0.3.37//:linkme_impl\",\n ],\n compile_data = glob(\n allow_empty = True,\n include = [\"**\"],\n exclude = [\n \"**/* *\",\n \".tmp_git_root/**/*\",\n \"BUILD\",\n \"BUILD.bazel\",\n \"WORKSPACE\",\n \"WORKSPACE.bazel\",\n ],\n ),\n crate_root = \"src/lib.rs\",\n edition = \"2021\",\n rustc_env_files = [\n \":cargo_toml_env_vars\",\n ],\n rustc_flags = [\n \"--cap-lints=allow\",\n ],\n srcs = glob(\n allow_empty = True,\n include = [\"**/*.rs\"],\n ),\n tags = [\n \"cargo-bazel\",\n \"crate-name=linkme\",\n \"manual\",\n \"noclippy\",\n \"norustfmt\",\n ],\n target_compatible_with = select({\n \"@rules_rust//rust/platform:aarch64-apple-darwin\": [],\n \"@rules_rust//rust/platform:aarch64-unknown-linux-gnu\": [],\n \"@rules_rust//rust/platform:wasm32-unknown-unknown\": [],\n \"@rules_rust//rust/platform:wasm32-wasip1\": [],\n \"@rules_rust//rust/platform:x86_64-pc-windows-msvc\": [],\n \"@rules_rust//rust/platform:x86_64-unknown-linux-gnu\": [],\n \"@rules_rust//rust/platform:x86_64-unknown-nixos-gnu\": [],\n \"//conditions:default\": [\"@platforms//:incompatible\"],\n }),\n version = \"0.3.37\",\n)\n\ncargo_build_script(\n name = \"_bs\",\n compile_data = glob(\n allow_empty = True,\n include = [\"**\"],\n exclude = [\n \"**/* *\",\n \"**/*.rs\",\n \".tmp_git_root/**/*\",\n \"BUILD\",\n \"BUILD.bazel\",\n \"WORKSPACE\",\n \"WORKSPACE.bazel\",\n ],\n ),\n crate_name = \"build_script_build\",\n crate_root = \"build.rs\",\n data = glob(\n allow_empty = True,\n include = [\"**\"],\n exclude = [\n \"**/* *\",\n \".tmp_git_root/**/*\",\n \"BUILD\",\n \"BUILD.bazel\",\n \"WORKSPACE\",\n \"WORKSPACE.bazel\",\n ],\n ),\n edition = \"2021\",\n pkg_name = \"linkme\",\n rustc_env_files = [\n \":cargo_toml_env_vars\",\n ],\n rustc_flags = [\n \"--cap-lints=allow\",\n ],\n srcs = glob(\n allow_empty = True,\n include = [\"**/*.rs\"],\n ),\n tags = [\n \"cargo-bazel\",\n \"crate-name=linkme\",\n \"manual\",\n \"noclippy\",\n \"norustfmt\",\n ],\n version = \"0.3.37\",\n visibility = [\"//visibility:private\"],\n)\n\nalias(\n name = \"build_script_build\",\n actual = \":_bs\",\n tags = [\"manual\"],\n)\n" + } + }, + "crates__linkme-impl-0.3.37": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "patch_args": [], + "patch_tool": "", + "patches": [], + "remote_patch_strip": 1, + "sha256": "77060ebe535362c3da75682cd17b0431017b6e7c5661e714fc69a7ad017d1301", + "type": "tar.gz", + "urls": [ + "https://static.crates.io/crates/linkme-impl/0.3.37/download" + ], + "strip_prefix": "linkme-impl-0.3.37", + "build_file_content": "###############################################################################\n# @generated\n# DO NOT MODIFY: This file is auto-generated by a crate_universe tool. To \n# regenerate this file, run the following:\n#\n# bazel mod show_repo 'protobuf'\n###############################################################################\n\nload(\n \"@rules_rust//cargo:defs.bzl\",\n \"cargo_build_script\",\n \"cargo_toml_env_vars\",\n)\n\nload(\"@rules_rust//rust:defs.bzl\", \"rust_proc_macro\")\n\n# buildifier: disable=bzl-visibility\nload(\"@rules_rust//crate_universe/private:selects.bzl\", \"selects\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\ncargo_toml_env_vars(\n name = \"cargo_toml_env_vars\",\n src = \"Cargo.toml\",\n)\n\nrust_proc_macro(\n name = \"linkme_impl\",\n deps = [\n \"@crates__linkme-impl-0.3.37//:build_script_build\",\n \"@crates__proc-macro2-1.0.107//:proc_macro2\",\n \"@crates__quote-1.0.47//:quote\",\n \"@crates__syn-3.0.9//:syn\",\n ],\n compile_data = glob(\n allow_empty = True,\n include = [\"**\"],\n exclude = [\n \"**/* *\",\n \".tmp_git_root/**/*\",\n \"BUILD\",\n \"BUILD.bazel\",\n \"WORKSPACE\",\n \"WORKSPACE.bazel\",\n ],\n ),\n crate_root = \"src/lib.rs\",\n edition = \"2021\",\n rustc_env_files = [\n \":cargo_toml_env_vars\",\n ],\n rustc_flags = [\n \"--cap-lints=allow\",\n ],\n srcs = glob(\n allow_empty = True,\n include = [\"**/*.rs\"],\n ),\n tags = [\n \"cargo-bazel\",\n \"crate-name=linkme-impl\",\n \"manual\",\n \"noclippy\",\n \"norustfmt\",\n ],\n target_compatible_with = select({\n \"@rules_rust//rust/platform:aarch64-apple-darwin\": [],\n \"@rules_rust//rust/platform:aarch64-unknown-linux-gnu\": [],\n \"@rules_rust//rust/platform:wasm32-unknown-unknown\": [],\n \"@rules_rust//rust/platform:wasm32-wasip1\": [],\n \"@rules_rust//rust/platform:x86_64-pc-windows-msvc\": [],\n \"@rules_rust//rust/platform:x86_64-unknown-linux-gnu\": [],\n \"@rules_rust//rust/platform:x86_64-unknown-nixos-gnu\": [],\n \"//conditions:default\": [\"@platforms//:incompatible\"],\n }),\n version = \"0.3.37\",\n)\n\ncargo_build_script(\n name = \"_bs\",\n compile_data = glob(\n allow_empty = True,\n include = [\"**\"],\n exclude = [\n \"**/* *\",\n \"**/*.rs\",\n \".tmp_git_root/**/*\",\n \"BUILD\",\n \"BUILD.bazel\",\n \"WORKSPACE\",\n \"WORKSPACE.bazel\",\n ],\n ),\n crate_name = \"build_script_build\",\n crate_root = \"build.rs\",\n data = glob(\n allow_empty = True,\n include = [\"**\"],\n exclude = [\n \"**/* *\",\n \".tmp_git_root/**/*\",\n \"BUILD\",\n \"BUILD.bazel\",\n \"WORKSPACE\",\n \"WORKSPACE.bazel\",\n ],\n ),\n edition = \"2021\",\n pkg_name = \"linkme-impl\",\n rustc_env_files = [\n \":cargo_toml_env_vars\",\n ],\n rustc_flags = [\n \"--cap-lints=allow\",\n ],\n srcs = glob(\n allow_empty = True,\n include = [\"**/*.rs\"],\n ),\n tags = [\n \"cargo-bazel\",\n \"crate-name=linkme-impl\",\n \"manual\",\n \"noclippy\",\n \"norustfmt\",\n ],\n version = \"0.3.37\",\n visibility = [\"//visibility:private\"],\n)\n\nalias(\n name = \"build_script_build\",\n actual = \":_bs\",\n tags = [\"manual\"],\n)\n" + } + }, + "crates__memchr-2.8.3": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "patch_args": [], + "patch_tool": "", + "patches": [], + "remote_patch_strip": 1, + "sha256": "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98", + "type": "tar.gz", + "urls": [ + "https://static.crates.io/crates/memchr/2.8.3/download" + ], + "strip_prefix": "memchr-2.8.3", + "build_file_content": "###############################################################################\n# @generated\n# DO NOT MODIFY: This file is auto-generated by a crate_universe tool. To \n# regenerate this file, run the following:\n#\n# bazel mod show_repo 'protobuf'\n###############################################################################\n\nload(\"@rules_rust//cargo:defs.bzl\", \"cargo_toml_env_vars\")\n\nload(\"@rules_rust//rust:defs.bzl\", \"rust_library\")\n\n# buildifier: disable=bzl-visibility\nload(\"@rules_rust//crate_universe/private:selects.bzl\", \"selects\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\ncargo_toml_env_vars(\n name = \"cargo_toml_env_vars\",\n src = \"Cargo.toml\",\n)\n\nrust_library(\n name = \"memchr\",\n compile_data = glob(\n allow_empty = True,\n include = [\"**\"],\n exclude = [\n \"**/* *\",\n \".tmp_git_root/**/*\",\n \"BUILD\",\n \"BUILD.bazel\",\n \"WORKSPACE\",\n \"WORKSPACE.bazel\",\n ],\n ),\n crate_features = [\n \"alloc\",\n \"std\",\n ],\n crate_root = \"src/lib.rs\",\n edition = \"2021\",\n rustc_env_files = [\n \":cargo_toml_env_vars\",\n ],\n rustc_flags = [\n \"--cap-lints=allow\",\n ],\n srcs = glob(\n allow_empty = True,\n include = [\"**/*.rs\"],\n ),\n tags = [\n \"cargo-bazel\",\n \"crate-name=memchr\",\n \"manual\",\n \"noclippy\",\n \"norustfmt\",\n ],\n target_compatible_with = select({\n \"@rules_rust//rust/platform:aarch64-apple-darwin\": [],\n \"@rules_rust//rust/platform:aarch64-unknown-linux-gnu\": [],\n \"@rules_rust//rust/platform:wasm32-unknown-unknown\": [],\n \"@rules_rust//rust/platform:wasm32-wasip1\": [],\n \"@rules_rust//rust/platform:x86_64-pc-windows-msvc\": [],\n \"@rules_rust//rust/platform:x86_64-unknown-linux-gnu\": [],\n \"@rules_rust//rust/platform:x86_64-unknown-nixos-gnu\": [],\n \"//conditions:default\": [\"@platforms//:incompatible\"],\n }),\n version = \"2.8.3\",\n)\n" + } + }, + "crates__num-traits-0.2.19": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "patch_args": [], + "patch_tool": "", + "patches": [], + "remote_patch_strip": 1, + "sha256": "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841", + "type": "tar.gz", + "urls": [ + "https://static.crates.io/crates/num-traits/0.2.19/download" + ], + "strip_prefix": "num-traits-0.2.19", + "build_file_content": "###############################################################################\n# @generated\n# DO NOT MODIFY: This file is auto-generated by a crate_universe tool. To \n# regenerate this file, run the following:\n#\n# bazel mod show_repo 'protobuf'\n###############################################################################\n\nload(\n \"@rules_rust//cargo:defs.bzl\",\n \"cargo_build_script\",\n \"cargo_toml_env_vars\",\n)\n\nload(\"@rules_rust//rust:defs.bzl\", \"rust_library\")\n\n# buildifier: disable=bzl-visibility\nload(\"@rules_rust//crate_universe/private:selects.bzl\", \"selects\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\ncargo_toml_env_vars(\n name = \"cargo_toml_env_vars\",\n src = \"Cargo.toml\",\n)\n\nrust_library(\n name = \"num_traits\",\n deps = [\n \"@crates__num-traits-0.2.19//:build_script_build\",\n ],\n compile_data = glob(\n allow_empty = True,\n include = [\"**\"],\n exclude = [\n \"**/* *\",\n \".tmp_git_root/**/*\",\n \"BUILD\",\n \"BUILD.bazel\",\n \"WORKSPACE\",\n \"WORKSPACE.bazel\",\n ],\n ),\n crate_features = [\n \"default\",\n \"std\",\n ],\n crate_root = \"src/lib.rs\",\n edition = \"2021\",\n rustc_env_files = [\n \":cargo_toml_env_vars\",\n ],\n rustc_flags = [\n \"--cap-lints=allow\",\n ],\n srcs = glob(\n allow_empty = True,\n include = [\"**/*.rs\"],\n ),\n tags = [\n \"cargo-bazel\",\n \"crate-name=num-traits\",\n \"manual\",\n \"noclippy\",\n \"norustfmt\",\n ],\n target_compatible_with = select({\n \"@rules_rust//rust/platform:aarch64-apple-darwin\": [],\n \"@rules_rust//rust/platform:aarch64-unknown-linux-gnu\": [],\n \"@rules_rust//rust/platform:wasm32-unknown-unknown\": [],\n \"@rules_rust//rust/platform:wasm32-wasip1\": [],\n \"@rules_rust//rust/platform:x86_64-pc-windows-msvc\": [],\n \"@rules_rust//rust/platform:x86_64-unknown-linux-gnu\": [],\n \"@rules_rust//rust/platform:x86_64-unknown-nixos-gnu\": [],\n \"//conditions:default\": [\"@platforms//:incompatible\"],\n }),\n version = \"0.2.19\",\n)\n\ncargo_build_script(\n name = \"_bs\",\n compile_data = glob(\n allow_empty = True,\n include = [\"**\"],\n exclude = [\n \"**/* *\",\n \"**/*.rs\",\n \".tmp_git_root/**/*\",\n \"BUILD\",\n \"BUILD.bazel\",\n \"WORKSPACE\",\n \"WORKSPACE.bazel\",\n ],\n ),\n crate_features = [\n \"default\",\n \"std\",\n ],\n crate_name = \"build_script_build\",\n crate_root = \"build.rs\",\n data = glob(\n allow_empty = True,\n include = [\"**\"],\n exclude = [\n \"**/* *\",\n \".tmp_git_root/**/*\",\n \"BUILD\",\n \"BUILD.bazel\",\n \"WORKSPACE\",\n \"WORKSPACE.bazel\",\n ],\n ),\n deps = [\n \"@crates__autocfg-1.5.1//:autocfg\",\n ],\n edition = \"2021\",\n pkg_name = \"num-traits\",\n rustc_env_files = [\n \":cargo_toml_env_vars\",\n ],\n rustc_flags = [\n \"--cap-lints=allow\",\n ],\n srcs = glob(\n allow_empty = True,\n include = [\"**/*.rs\"],\n ),\n tags = [\n \"cargo-bazel\",\n \"crate-name=num-traits\",\n \"manual\",\n \"noclippy\",\n \"norustfmt\",\n ],\n version = \"0.2.19\",\n visibility = [\"//visibility:private\"],\n)\n\nalias(\n name = \"build_script_build\",\n actual = \":_bs\",\n tags = [\"manual\"],\n)\n" + } + }, + "crates__paste-1.0.15": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "patch_args": [], + "patch_tool": "", + "patches": [], + "remote_patch_strip": 1, + "sha256": "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a", + "type": "tar.gz", + "urls": [ + "https://static.crates.io/crates/paste/1.0.15/download" + ], + "strip_prefix": "paste-1.0.15", + "build_file_content": "###############################################################################\n# @generated\n# DO NOT MODIFY: This file is auto-generated by a crate_universe tool. To \n# regenerate this file, run the following:\n#\n# bazel mod show_repo 'protobuf'\n###############################################################################\n\nload(\n \"@rules_rust//cargo:defs.bzl\",\n \"cargo_build_script\",\n \"cargo_toml_env_vars\",\n)\n\nload(\"@rules_rust//rust:defs.bzl\", \"rust_proc_macro\")\n\n# buildifier: disable=bzl-visibility\nload(\"@rules_rust//crate_universe/private:selects.bzl\", \"selects\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\ncargo_toml_env_vars(\n name = \"cargo_toml_env_vars\",\n src = \"Cargo.toml\",\n)\n\nrust_proc_macro(\n name = \"paste\",\n deps = [\n \"@crates__paste-1.0.15//:build_script_build\",\n ],\n compile_data = glob(\n allow_empty = True,\n include = [\"**\"],\n exclude = [\n \"**/* *\",\n \".tmp_git_root/**/*\",\n \"BUILD\",\n \"BUILD.bazel\",\n \"WORKSPACE\",\n \"WORKSPACE.bazel\",\n ],\n ),\n crate_root = \"src/lib.rs\",\n edition = \"2018\",\n rustc_env_files = [\n \":cargo_toml_env_vars\",\n ],\n rustc_flags = [\n \"--cap-lints=allow\",\n ],\n srcs = glob(\n allow_empty = True,\n include = [\"**/*.rs\"],\n ),\n tags = [\n \"cargo-bazel\",\n \"crate-name=paste\",\n \"manual\",\n \"noclippy\",\n \"norustfmt\",\n ],\n target_compatible_with = select({\n \"@rules_rust//rust/platform:aarch64-apple-darwin\": [],\n \"@rules_rust//rust/platform:aarch64-unknown-linux-gnu\": [],\n \"@rules_rust//rust/platform:wasm32-unknown-unknown\": [],\n \"@rules_rust//rust/platform:wasm32-wasip1\": [],\n \"@rules_rust//rust/platform:x86_64-pc-windows-msvc\": [],\n \"@rules_rust//rust/platform:x86_64-unknown-linux-gnu\": [],\n \"@rules_rust//rust/platform:x86_64-unknown-nixos-gnu\": [],\n \"//conditions:default\": [\"@platforms//:incompatible\"],\n }),\n version = \"1.0.15\",\n)\n\ncargo_build_script(\n name = \"_bs\",\n compile_data = glob(\n allow_empty = True,\n include = [\"**\"],\n exclude = [\n \"**/* *\",\n \"**/*.rs\",\n \".tmp_git_root/**/*\",\n \"BUILD\",\n \"BUILD.bazel\",\n \"WORKSPACE\",\n \"WORKSPACE.bazel\",\n ],\n ),\n crate_name = \"build_script_build\",\n crate_root = \"build.rs\",\n data = glob(\n allow_empty = True,\n include = [\"**\"],\n exclude = [\n \"**/* *\",\n \".tmp_git_root/**/*\",\n \"BUILD\",\n \"BUILD.bazel\",\n \"WORKSPACE\",\n \"WORKSPACE.bazel\",\n ],\n ),\n edition = \"2018\",\n pkg_name = \"paste\",\n rustc_env_files = [\n \":cargo_toml_env_vars\",\n ],\n rustc_flags = [\n \"--cap-lints=allow\",\n ],\n srcs = glob(\n allow_empty = True,\n include = [\"**/*.rs\"],\n ),\n tags = [\n \"cargo-bazel\",\n \"crate-name=paste\",\n \"manual\",\n \"noclippy\",\n \"norustfmt\",\n ],\n version = \"1.0.15\",\n visibility = [\"//visibility:private\"],\n)\n\nalias(\n name = \"build_script_build\",\n actual = \":_bs\",\n tags = [\"manual\"],\n)\n" + } + }, + "crates__proc-macro2-1.0.107": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "patch_args": [], + "patch_tool": "", + "patches": [], + "remote_patch_strip": 1, + "sha256": "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9", + "type": "tar.gz", + "urls": [ + "https://static.crates.io/crates/proc-macro2/1.0.107/download" + ], + "strip_prefix": "proc-macro2-1.0.107", + "build_file_content": "###############################################################################\n# @generated\n# DO NOT MODIFY: This file is auto-generated by a crate_universe tool. To \n# regenerate this file, run the following:\n#\n# bazel mod show_repo 'protobuf'\n###############################################################################\n\nload(\n \"@rules_rust//cargo:defs.bzl\",\n \"cargo_build_script\",\n \"cargo_toml_env_vars\",\n)\n\nload(\"@rules_rust//rust:defs.bzl\", \"rust_library\")\n\n# buildifier: disable=bzl-visibility\nload(\"@rules_rust//crate_universe/private:selects.bzl\", \"selects\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\ncargo_toml_env_vars(\n name = \"cargo_toml_env_vars\",\n src = \"Cargo.toml\",\n)\n\nrust_library(\n name = \"proc_macro2\",\n deps = [\n \"@crates__proc-macro2-1.0.107//:build_script_build\",\n \"@crates__unicode-ident-1.0.27//:unicode_ident\",\n ],\n compile_data = glob(\n allow_empty = True,\n include = [\"**\"],\n exclude = [\n \"**/* *\",\n \".tmp_git_root/**/*\",\n \"BUILD\",\n \"BUILD.bazel\",\n \"WORKSPACE\",\n \"WORKSPACE.bazel\",\n ],\n ),\n crate_features = [\n \"proc-macro\",\n ] + select({\n \"@rules_rust//rust/platform:aarch64-apple-darwin\": [\n \"default\", # aarch64-apple-darwin\n ],\n \"@rules_rust//rust/platform:aarch64-unknown-linux-gnu\": [\n \"default\", # aarch64-unknown-linux-gnu\n ],\n \"@rules_rust//rust/platform:x86_64-pc-windows-msvc\": [\n \"default\", # x86_64-pc-windows-msvc\n ],\n \"@rules_rust//rust/platform:x86_64-unknown-linux-gnu\": [\n \"default\", # x86_64-unknown-linux-gnu\n ],\n \"@rules_rust//rust/platform:x86_64-unknown-nixos-gnu\": [\n \"default\", # x86_64-unknown-nixos-gnu\n ],\n \"//conditions:default\": [],\n }),\n crate_root = \"src/lib.rs\",\n edition = \"2021\",\n rustc_env_files = [\n \":cargo_toml_env_vars\",\n ],\n rustc_flags = [\n \"--cap-lints=allow\",\n ],\n srcs = glob(\n allow_empty = True,\n include = [\"**/*.rs\"],\n ),\n tags = [\n \"cargo-bazel\",\n \"crate-name=proc-macro2\",\n \"manual\",\n \"noclippy\",\n \"norustfmt\",\n ],\n target_compatible_with = select({\n \"@rules_rust//rust/platform:aarch64-apple-darwin\": [],\n \"@rules_rust//rust/platform:aarch64-unknown-linux-gnu\": [],\n \"@rules_rust//rust/platform:wasm32-unknown-unknown\": [],\n \"@rules_rust//rust/platform:wasm32-wasip1\": [],\n \"@rules_rust//rust/platform:x86_64-pc-windows-msvc\": [],\n \"@rules_rust//rust/platform:x86_64-unknown-linux-gnu\": [],\n \"@rules_rust//rust/platform:x86_64-unknown-nixos-gnu\": [],\n \"//conditions:default\": [\"@platforms//:incompatible\"],\n }),\n version = \"1.0.107\",\n)\n\ncargo_build_script(\n name = \"_bs\",\n compile_data = glob(\n allow_empty = True,\n include = [\"**\"],\n exclude = [\n \"**/* *\",\n \"**/*.rs\",\n \".tmp_git_root/**/*\",\n \"BUILD\",\n \"BUILD.bazel\",\n \"WORKSPACE\",\n \"WORKSPACE.bazel\",\n ],\n ),\n crate_features = [\n \"proc-macro\",\n ] + select({\n \"@rules_rust//rust/platform:aarch64-apple-darwin\": [\n \"default\", # aarch64-apple-darwin\n ],\n \"@rules_rust//rust/platform:aarch64-unknown-linux-gnu\": [\n \"default\", # aarch64-unknown-linux-gnu\n ],\n \"@rules_rust//rust/platform:x86_64-pc-windows-msvc\": [\n \"default\", # x86_64-pc-windows-msvc\n ],\n \"@rules_rust//rust/platform:x86_64-unknown-linux-gnu\": [\n \"default\", # x86_64-unknown-linux-gnu\n ],\n \"@rules_rust//rust/platform:x86_64-unknown-nixos-gnu\": [\n \"default\", # x86_64-unknown-nixos-gnu\n ],\n \"//conditions:default\": [],\n }),\n crate_name = \"build_script_build\",\n crate_root = \"build.rs\",\n data = glob(\n allow_empty = True,\n include = [\"**\"],\n exclude = [\n \"**/* *\",\n \".tmp_git_root/**/*\",\n \"BUILD\",\n \"BUILD.bazel\",\n \"WORKSPACE\",\n \"WORKSPACE.bazel\",\n ],\n ),\n edition = \"2021\",\n pkg_name = \"proc-macro2\",\n rustc_env_files = [\n \":cargo_toml_env_vars\",\n ],\n rustc_flags = [\n \"--cap-lints=allow\",\n ],\n srcs = glob(\n allow_empty = True,\n include = [\"**/*.rs\"],\n ),\n tags = [\n \"cargo-bazel\",\n \"crate-name=proc-macro2\",\n \"manual\",\n \"noclippy\",\n \"norustfmt\",\n ],\n version = \"1.0.107\",\n visibility = [\"//visibility:private\"],\n)\n\nalias(\n name = \"build_script_build\",\n actual = \":_bs\",\n tags = [\"manual\"],\n)\n" + } + }, + "crates__quote-1.0.47": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "patch_args": [], + "patch_tool": "", + "patches": [], + "remote_patch_strip": 1, + "sha256": "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001", + "type": "tar.gz", + "urls": [ + "https://static.crates.io/crates/quote/1.0.47/download" + ], + "strip_prefix": "quote-1.0.47", + "build_file_content": "###############################################################################\n# @generated\n# DO NOT MODIFY: This file is auto-generated by a crate_universe tool. To \n# regenerate this file, run the following:\n#\n# bazel mod show_repo 'protobuf'\n###############################################################################\n\nload(\n \"@rules_rust//cargo:defs.bzl\",\n \"cargo_build_script\",\n \"cargo_toml_env_vars\",\n)\n\nload(\"@rules_rust//rust:defs.bzl\", \"rust_library\")\n\n# buildifier: disable=bzl-visibility\nload(\"@rules_rust//crate_universe/private:selects.bzl\", \"selects\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\ncargo_toml_env_vars(\n name = \"cargo_toml_env_vars\",\n src = \"Cargo.toml\",\n)\n\nrust_library(\n name = \"quote\",\n deps = [\n \"@crates__proc-macro2-1.0.107//:proc_macro2\",\n \"@crates__quote-1.0.47//:build_script_build\",\n ],\n compile_data = glob(\n allow_empty = True,\n include = [\"**\"],\n exclude = [\n \"**/* *\",\n \".tmp_git_root/**/*\",\n \"BUILD\",\n \"BUILD.bazel\",\n \"WORKSPACE\",\n \"WORKSPACE.bazel\",\n ],\n ),\n crate_features = [\n \"default\",\n \"proc-macro\",\n ],\n crate_root = \"src/lib.rs\",\n edition = \"2021\",\n rustc_env_files = [\n \":cargo_toml_env_vars\",\n ],\n rustc_flags = [\n \"--cap-lints=allow\",\n ],\n srcs = glob(\n allow_empty = True,\n include = [\"**/*.rs\"],\n ),\n tags = [\n \"cargo-bazel\",\n \"crate-name=quote\",\n \"manual\",\n \"noclippy\",\n \"norustfmt\",\n ],\n target_compatible_with = select({\n \"@rules_rust//rust/platform:aarch64-apple-darwin\": [],\n \"@rules_rust//rust/platform:aarch64-unknown-linux-gnu\": [],\n \"@rules_rust//rust/platform:wasm32-unknown-unknown\": [],\n \"@rules_rust//rust/platform:wasm32-wasip1\": [],\n \"@rules_rust//rust/platform:x86_64-pc-windows-msvc\": [],\n \"@rules_rust//rust/platform:x86_64-unknown-linux-gnu\": [],\n \"@rules_rust//rust/platform:x86_64-unknown-nixos-gnu\": [],\n \"//conditions:default\": [\"@platforms//:incompatible\"],\n }),\n version = \"1.0.47\",\n)\n\ncargo_build_script(\n name = \"_bs\",\n compile_data = glob(\n allow_empty = True,\n include = [\"**\"],\n exclude = [\n \"**/* *\",\n \"**/*.rs\",\n \".tmp_git_root/**/*\",\n \"BUILD\",\n \"BUILD.bazel\",\n \"WORKSPACE\",\n \"WORKSPACE.bazel\",\n ],\n ),\n crate_features = [\n \"default\",\n \"proc-macro\",\n ],\n crate_name = \"build_script_build\",\n crate_root = \"build.rs\",\n data = glob(\n allow_empty = True,\n include = [\"**\"],\n exclude = [\n \"**/* *\",\n \".tmp_git_root/**/*\",\n \"BUILD\",\n \"BUILD.bazel\",\n \"WORKSPACE\",\n \"WORKSPACE.bazel\",\n ],\n ),\n edition = \"2021\",\n pkg_name = \"quote\",\n rustc_env_files = [\n \":cargo_toml_env_vars\",\n ],\n rustc_flags = [\n \"--cap-lints=allow\",\n ],\n srcs = glob(\n allow_empty = True,\n include = [\"**/*.rs\"],\n ),\n tags = [\n \"cargo-bazel\",\n \"crate-name=quote\",\n \"manual\",\n \"noclippy\",\n \"norustfmt\",\n ],\n version = \"1.0.47\",\n visibility = [\"//visibility:private\"],\n)\n\nalias(\n name = \"build_script_build\",\n actual = \":_bs\",\n tags = [\"manual\"],\n)\n" + } + }, + "crates__regex-1.13.1": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "patch_args": [], + "patch_tool": "", + "patches": [], + "remote_patch_strip": 1, + "sha256": "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d", + "type": "tar.gz", + "urls": [ + "https://static.crates.io/crates/regex/1.13.1/download" + ], + "strip_prefix": "regex-1.13.1", + "build_file_content": "###############################################################################\n# @generated\n# DO NOT MODIFY: This file is auto-generated by a crate_universe tool. To \n# regenerate this file, run the following:\n#\n# bazel mod show_repo 'protobuf'\n###############################################################################\n\nload(\"@rules_rust//cargo:defs.bzl\", \"cargo_toml_env_vars\")\n\nload(\"@rules_rust//rust:defs.bzl\", \"rust_library\")\n\n# buildifier: disable=bzl-visibility\nload(\"@rules_rust//crate_universe/private:selects.bzl\", \"selects\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\ncargo_toml_env_vars(\n name = \"cargo_toml_env_vars\",\n src = \"Cargo.toml\",\n)\n\nrust_library(\n name = \"regex\",\n deps = [\n \"@crates__aho-corasick-1.1.5//:aho_corasick\",\n \"@crates__memchr-2.8.3//:memchr\",\n \"@crates__regex-automata-0.4.18//:regex_automata\",\n \"@crates__regex-syntax-0.8.11//:regex_syntax\",\n ],\n compile_data = glob(\n allow_empty = True,\n include = [\"**\"],\n exclude = [\n \"**/* *\",\n \".tmp_git_root/**/*\",\n \"BUILD\",\n \"BUILD.bazel\",\n \"WORKSPACE\",\n \"WORKSPACE.bazel\",\n ],\n ),\n crate_features = [\n \"default\",\n \"perf\",\n \"perf-backtrack\",\n \"perf-cache\",\n \"perf-dfa\",\n \"perf-inline\",\n \"perf-literal\",\n \"perf-onepass\",\n \"std\",\n \"unicode\",\n \"unicode-age\",\n \"unicode-bool\",\n \"unicode-case\",\n \"unicode-gencat\",\n \"unicode-perl\",\n \"unicode-script\",\n \"unicode-segment\",\n ],\n crate_root = \"src/lib.rs\",\n edition = \"2021\",\n rustc_env_files = [\n \":cargo_toml_env_vars\",\n ],\n rustc_flags = [\n \"--cap-lints=allow\",\n ],\n srcs = glob(\n allow_empty = True,\n include = [\"**/*.rs\"],\n ),\n tags = [\n \"cargo-bazel\",\n \"crate-name=regex\",\n \"manual\",\n \"noclippy\",\n \"norustfmt\",\n ],\n target_compatible_with = select({\n \"@rules_rust//rust/platform:aarch64-apple-darwin\": [],\n \"@rules_rust//rust/platform:aarch64-unknown-linux-gnu\": [],\n \"@rules_rust//rust/platform:wasm32-unknown-unknown\": [],\n \"@rules_rust//rust/platform:wasm32-wasip1\": [],\n \"@rules_rust//rust/platform:x86_64-pc-windows-msvc\": [],\n \"@rules_rust//rust/platform:x86_64-unknown-linux-gnu\": [],\n \"@rules_rust//rust/platform:x86_64-unknown-nixos-gnu\": [],\n \"//conditions:default\": [\"@platforms//:incompatible\"],\n }),\n version = \"1.13.1\",\n)\n" + } + }, + "crates__regex-automata-0.4.18": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "patch_args": [], + "patch_tool": "", + "patches": [], + "remote_patch_strip": 1, + "sha256": "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2", + "type": "tar.gz", + "urls": [ + "https://static.crates.io/crates/regex-automata/0.4.18/download" + ], + "strip_prefix": "regex-automata-0.4.18", + "build_file_content": "###############################################################################\n# @generated\n# DO NOT MODIFY: This file is auto-generated by a crate_universe tool. To \n# regenerate this file, run the following:\n#\n# bazel mod show_repo 'protobuf'\n###############################################################################\n\nload(\"@rules_rust//cargo:defs.bzl\", \"cargo_toml_env_vars\")\n\nload(\"@rules_rust//rust:defs.bzl\", \"rust_library\")\n\n# buildifier: disable=bzl-visibility\nload(\"@rules_rust//crate_universe/private:selects.bzl\", \"selects\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\ncargo_toml_env_vars(\n name = \"cargo_toml_env_vars\",\n src = \"Cargo.toml\",\n)\n\nrust_library(\n name = \"regex_automata\",\n deps = [\n \"@crates__aho-corasick-1.1.5//:aho_corasick\",\n \"@crates__memchr-2.8.3//:memchr\",\n \"@crates__regex-syntax-0.8.11//:regex_syntax\",\n ],\n compile_data = glob(\n allow_empty = True,\n include = [\"**\"],\n exclude = [\n \"**/* *\",\n \".tmp_git_root/**/*\",\n \"BUILD\",\n \"BUILD.bazel\",\n \"WORKSPACE\",\n \"WORKSPACE.bazel\",\n ],\n ),\n crate_features = [\n \"alloc\",\n \"dfa-onepass\",\n \"hybrid\",\n \"meta\",\n \"nfa-backtrack\",\n \"nfa-pikevm\",\n \"nfa-thompson\",\n \"perf-inline\",\n \"perf-literal\",\n \"perf-literal-multisubstring\",\n \"perf-literal-substring\",\n \"std\",\n \"syntax\",\n \"unicode\",\n \"unicode-age\",\n \"unicode-bool\",\n \"unicode-case\",\n \"unicode-gencat\",\n \"unicode-perl\",\n \"unicode-script\",\n \"unicode-segment\",\n \"unicode-word-boundary\",\n ],\n crate_root = \"src/lib.rs\",\n edition = \"2021\",\n rustc_env_files = [\n \":cargo_toml_env_vars\",\n ],\n rustc_flags = [\n \"--cap-lints=allow\",\n ],\n srcs = glob(\n allow_empty = True,\n include = [\"**/*.rs\"],\n ),\n tags = [\n \"cargo-bazel\",\n \"crate-name=regex-automata\",\n \"manual\",\n \"noclippy\",\n \"norustfmt\",\n ],\n target_compatible_with = select({\n \"@rules_rust//rust/platform:aarch64-apple-darwin\": [],\n \"@rules_rust//rust/platform:aarch64-unknown-linux-gnu\": [],\n \"@rules_rust//rust/platform:wasm32-unknown-unknown\": [],\n \"@rules_rust//rust/platform:wasm32-wasip1\": [],\n \"@rules_rust//rust/platform:x86_64-pc-windows-msvc\": [],\n \"@rules_rust//rust/platform:x86_64-unknown-linux-gnu\": [],\n \"@rules_rust//rust/platform:x86_64-unknown-nixos-gnu\": [],\n \"//conditions:default\": [\"@platforms//:incompatible\"],\n }),\n version = \"0.4.18\",\n)\n" + } + }, + "crates__regex-syntax-0.8.11": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "patch_args": [], + "patch_tool": "", + "patches": [], + "remote_patch_strip": 1, + "sha256": "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4", + "type": "tar.gz", + "urls": [ + "https://static.crates.io/crates/regex-syntax/0.8.11/download" + ], + "strip_prefix": "regex-syntax-0.8.11", + "build_file_content": "###############################################################################\n# @generated\n# DO NOT MODIFY: This file is auto-generated by a crate_universe tool. To \n# regenerate this file, run the following:\n#\n# bazel mod show_repo 'protobuf'\n###############################################################################\n\nload(\"@rules_rust//cargo:defs.bzl\", \"cargo_toml_env_vars\")\n\nload(\"@rules_rust//rust:defs.bzl\", \"rust_library\")\n\n# buildifier: disable=bzl-visibility\nload(\"@rules_rust//crate_universe/private:selects.bzl\", \"selects\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\ncargo_toml_env_vars(\n name = \"cargo_toml_env_vars\",\n src = \"Cargo.toml\",\n)\n\nrust_library(\n name = \"regex_syntax\",\n compile_data = glob(\n allow_empty = True,\n include = [\"**\"],\n exclude = [\n \"**/* *\",\n \".tmp_git_root/**/*\",\n \"BUILD\",\n \"BUILD.bazel\",\n \"WORKSPACE\",\n \"WORKSPACE.bazel\",\n ],\n ),\n crate_features = [\n \"default\",\n \"std\",\n \"unicode\",\n \"unicode-age\",\n \"unicode-bool\",\n \"unicode-case\",\n \"unicode-gencat\",\n \"unicode-perl\",\n \"unicode-script\",\n \"unicode-segment\",\n ],\n crate_root = \"src/lib.rs\",\n edition = \"2021\",\n rustc_env_files = [\n \":cargo_toml_env_vars\",\n ],\n rustc_flags = [\n \"--cap-lints=allow\",\n ],\n srcs = glob(\n allow_empty = True,\n include = [\"**/*.rs\"],\n ),\n tags = [\n \"cargo-bazel\",\n \"crate-name=regex-syntax\",\n \"manual\",\n \"noclippy\",\n \"norustfmt\",\n ],\n target_compatible_with = select({\n \"@rules_rust//rust/platform:aarch64-apple-darwin\": [],\n \"@rules_rust//rust/platform:aarch64-unknown-linux-gnu\": [],\n \"@rules_rust//rust/platform:wasm32-unknown-unknown\": [],\n \"@rules_rust//rust/platform:wasm32-wasip1\": [],\n \"@rules_rust//rust/platform:x86_64-pc-windows-msvc\": [],\n \"@rules_rust//rust/platform:x86_64-unknown-linux-gnu\": [],\n \"@rules_rust//rust/platform:x86_64-unknown-nixos-gnu\": [],\n \"//conditions:default\": [\"@platforms//:incompatible\"],\n }),\n version = \"0.8.11\",\n)\n" + } + }, + "crates__rustversion-1.0.23": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "patch_args": [], + "patch_tool": "", + "patches": [], + "remote_patch_strip": 1, + "sha256": "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f", + "type": "tar.gz", + "urls": [ + "https://static.crates.io/crates/rustversion/1.0.23/download" + ], + "strip_prefix": "rustversion-1.0.23", + "build_file_content": "###############################################################################\n# @generated\n# DO NOT MODIFY: This file is auto-generated by a crate_universe tool. To \n# regenerate this file, run the following:\n#\n# bazel mod show_repo 'protobuf'\n###############################################################################\n\nload(\n \"@rules_rust//cargo:defs.bzl\",\n \"cargo_build_script\",\n \"cargo_toml_env_vars\",\n)\n\nload(\"@rules_rust//rust:defs.bzl\", \"rust_proc_macro\")\n\n# buildifier: disable=bzl-visibility\nload(\"@rules_rust//crate_universe/private:selects.bzl\", \"selects\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\ncargo_toml_env_vars(\n name = \"cargo_toml_env_vars\",\n src = \"Cargo.toml\",\n)\n\nrust_proc_macro(\n name = \"rustversion\",\n deps = [\n \"@crates__rustversion-1.0.23//:build_script_build\",\n ],\n compile_data = glob(\n allow_empty = True,\n include = [\"**\"],\n exclude = [\n \"**/* *\",\n \".tmp_git_root/**/*\",\n \"BUILD\",\n \"BUILD.bazel\",\n \"WORKSPACE\",\n \"WORKSPACE.bazel\",\n ],\n ),\n crate_root = \"src/lib.rs\",\n edition = \"2018\",\n rustc_env_files = [\n \":cargo_toml_env_vars\",\n ],\n rustc_flags = [\n \"--cap-lints=allow\",\n ],\n srcs = glob(\n allow_empty = True,\n include = [\"**/*.rs\"],\n ),\n tags = [\n \"cargo-bazel\",\n \"crate-name=rustversion\",\n \"manual\",\n \"noclippy\",\n \"norustfmt\",\n ],\n target_compatible_with = select({\n \"@rules_rust//rust/platform:aarch64-apple-darwin\": [],\n \"@rules_rust//rust/platform:aarch64-unknown-linux-gnu\": [],\n \"@rules_rust//rust/platform:wasm32-unknown-unknown\": [],\n \"@rules_rust//rust/platform:wasm32-wasip1\": [],\n \"@rules_rust//rust/platform:x86_64-pc-windows-msvc\": [],\n \"@rules_rust//rust/platform:x86_64-unknown-linux-gnu\": [],\n \"@rules_rust//rust/platform:x86_64-unknown-nixos-gnu\": [],\n \"//conditions:default\": [\"@platforms//:incompatible\"],\n }),\n version = \"1.0.23\",\n)\n\ncargo_build_script(\n name = \"_bs\",\n compile_data = glob(\n allow_empty = True,\n include = [\"**\"],\n exclude = [\n \"**/* *\",\n \"**/*.rs\",\n \".tmp_git_root/**/*\",\n \"BUILD\",\n \"BUILD.bazel\",\n \"WORKSPACE\",\n \"WORKSPACE.bazel\",\n ],\n ),\n crate_name = \"build_script_build\",\n crate_root = \"build/build.rs\",\n data = glob(\n allow_empty = True,\n include = [\"**\"],\n exclude = [\n \"**/* *\",\n \".tmp_git_root/**/*\",\n \"BUILD\",\n \"BUILD.bazel\",\n \"WORKSPACE\",\n \"WORKSPACE.bazel\",\n ],\n ),\n edition = \"2018\",\n pkg_name = \"rustversion\",\n rustc_env_files = [\n \":cargo_toml_env_vars\",\n ],\n rustc_flags = [\n \"--cap-lints=allow\",\n ],\n srcs = glob(\n allow_empty = True,\n include = [\"**/*.rs\"],\n ),\n tags = [\n \"cargo-bazel\",\n \"crate-name=rustversion\",\n \"manual\",\n \"noclippy\",\n \"norustfmt\",\n ],\n version = \"1.0.23\",\n visibility = [\"//visibility:private\"],\n)\n\nalias(\n name = \"build_script_build\",\n actual = \":_bs\",\n tags = [\"manual\"],\n)\n" + } + }, + "crates__syn-2.0.119": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "patch_args": [], + "patch_tool": "", + "patches": [], + "remote_patch_strip": 1, + "sha256": "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297", + "type": "tar.gz", + "urls": [ + "https://static.crates.io/crates/syn/2.0.119/download" + ], + "strip_prefix": "syn-2.0.119", + "build_file_content": "###############################################################################\n# @generated\n# DO NOT MODIFY: This file is auto-generated by a crate_universe tool. To \n# regenerate this file, run the following:\n#\n# bazel mod show_repo 'protobuf'\n###############################################################################\n\nload(\"@rules_rust//cargo:defs.bzl\", \"cargo_toml_env_vars\")\n\nload(\"@rules_rust//rust:defs.bzl\", \"rust_library\")\n\n# buildifier: disable=bzl-visibility\nload(\"@rules_rust//crate_universe/private:selects.bzl\", \"selects\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\ncargo_toml_env_vars(\n name = \"cargo_toml_env_vars\",\n src = \"Cargo.toml\",\n)\n\nrust_library(\n name = \"syn\",\n deps = [\n \"@crates__proc-macro2-1.0.107//:proc_macro2\",\n \"@crates__quote-1.0.47//:quote\",\n \"@crates__unicode-ident-1.0.27//:unicode_ident\",\n ],\n compile_data = glob(\n allow_empty = True,\n include = [\"**\"],\n exclude = [\n \"**/* *\",\n \".tmp_git_root/**/*\",\n \"BUILD\",\n \"BUILD.bazel\",\n \"WORKSPACE\",\n \"WORKSPACE.bazel\",\n ],\n ),\n crate_features = [\n \"clone-impls\",\n \"default\",\n \"derive\",\n \"parsing\",\n \"printing\",\n \"proc-macro\",\n ] + select({\n \"@rules_rust//rust/platform:aarch64-apple-darwin\": [\n \"extra-traits\", # aarch64-apple-darwin\n \"full\", # aarch64-apple-darwin\n ],\n \"@rules_rust//rust/platform:aarch64-unknown-linux-gnu\": [\n \"extra-traits\", # aarch64-unknown-linux-gnu\n \"full\", # aarch64-unknown-linux-gnu\n ],\n \"@rules_rust//rust/platform:x86_64-pc-windows-msvc\": [\n \"extra-traits\", # x86_64-pc-windows-msvc\n \"full\", # x86_64-pc-windows-msvc\n ],\n \"@rules_rust//rust/platform:x86_64-unknown-linux-gnu\": [\n \"extra-traits\", # x86_64-unknown-linux-gnu\n \"full\", # x86_64-unknown-linux-gnu\n ],\n \"@rules_rust//rust/platform:x86_64-unknown-nixos-gnu\": [\n \"extra-traits\", # x86_64-unknown-nixos-gnu\n \"full\", # x86_64-unknown-nixos-gnu\n ],\n \"//conditions:default\": [],\n }),\n crate_root = \"src/lib.rs\",\n edition = \"2021\",\n rustc_env_files = [\n \":cargo_toml_env_vars\",\n ],\n rustc_flags = [\n \"--cap-lints=allow\",\n ],\n srcs = glob(\n allow_empty = True,\n include = [\"**/*.rs\"],\n ),\n tags = [\n \"cargo-bazel\",\n \"crate-name=syn\",\n \"manual\",\n \"noclippy\",\n \"norustfmt\",\n ],\n target_compatible_with = select({\n \"@rules_rust//rust/platform:aarch64-apple-darwin\": [],\n \"@rules_rust//rust/platform:aarch64-unknown-linux-gnu\": [],\n \"@rules_rust//rust/platform:wasm32-unknown-unknown\": [],\n \"@rules_rust//rust/platform:wasm32-wasip1\": [],\n \"@rules_rust//rust/platform:x86_64-pc-windows-msvc\": [],\n \"@rules_rust//rust/platform:x86_64-unknown-linux-gnu\": [],\n \"@rules_rust//rust/platform:x86_64-unknown-nixos-gnu\": [],\n \"//conditions:default\": [\"@platforms//:incompatible\"],\n }),\n version = \"2.0.119\",\n)\n" + } + }, + "crates__syn-3.0.9": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "patch_args": [], + "patch_tool": "", + "patches": [], + "remote_patch_strip": 1, + "sha256": "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b", + "type": "tar.gz", + "urls": [ + "https://static.crates.io/crates/syn/3.0.9/download" + ], + "strip_prefix": "syn-3.0.9", + "build_file_content": "###############################################################################\n# @generated\n# DO NOT MODIFY: This file is auto-generated by a crate_universe tool. To \n# regenerate this file, run the following:\n#\n# bazel mod show_repo 'protobuf'\n###############################################################################\n\nload(\"@rules_rust//cargo:defs.bzl\", \"cargo_toml_env_vars\")\n\nload(\"@rules_rust//rust:defs.bzl\", \"rust_library\")\n\n# buildifier: disable=bzl-visibility\nload(\"@rules_rust//crate_universe/private:selects.bzl\", \"selects\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\ncargo_toml_env_vars(\n name = \"cargo_toml_env_vars\",\n src = \"Cargo.toml\",\n)\n\nrust_library(\n name = \"syn\",\n deps = [\n \"@crates__proc-macro2-1.0.107//:proc_macro2\",\n \"@crates__quote-1.0.47//:quote\",\n \"@crates__unicode-ident-1.0.27//:unicode_ident\",\n ],\n compile_data = glob(\n allow_empty = True,\n include = [\"**\"],\n exclude = [\n \"**/* *\",\n \".tmp_git_root/**/*\",\n \"BUILD\",\n \"BUILD.bazel\",\n \"WORKSPACE\",\n \"WORKSPACE.bazel\",\n ],\n ),\n crate_features = [\n \"clone-impls\",\n \"default\",\n \"derive\",\n \"parsing\",\n \"printing\",\n \"proc-macro\",\n ],\n crate_root = \"src/lib.rs\",\n edition = \"2021\",\n rustc_env_files = [\n \":cargo_toml_env_vars\",\n ],\n rustc_flags = [\n \"--cap-lints=allow\",\n ],\n srcs = glob(\n allow_empty = True,\n include = [\"**/*.rs\"],\n ),\n tags = [\n \"cargo-bazel\",\n \"crate-name=syn\",\n \"manual\",\n \"noclippy\",\n \"norustfmt\",\n ],\n target_compatible_with = select({\n \"@rules_rust//rust/platform:aarch64-apple-darwin\": [],\n \"@rules_rust//rust/platform:aarch64-unknown-linux-gnu\": [],\n \"@rules_rust//rust/platform:wasm32-unknown-unknown\": [],\n \"@rules_rust//rust/platform:wasm32-wasip1\": [],\n \"@rules_rust//rust/platform:x86_64-pc-windows-msvc\": [],\n \"@rules_rust//rust/platform:x86_64-unknown-linux-gnu\": [],\n \"@rules_rust//rust/platform:x86_64-unknown-nixos-gnu\": [],\n \"//conditions:default\": [\"@platforms//:incompatible\"],\n }),\n version = \"3.0.9\",\n)\n" + } + }, + "crates__unicode-ident-1.0.27": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "patch_args": [], + "patch_tool": "", + "patches": [], + "remote_patch_strip": 1, + "sha256": "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563", + "type": "tar.gz", + "urls": [ + "https://static.crates.io/crates/unicode-ident/1.0.27/download" + ], + "strip_prefix": "unicode-ident-1.0.27", + "build_file_content": "###############################################################################\n# @generated\n# DO NOT MODIFY: This file is auto-generated by a crate_universe tool. To \n# regenerate this file, run the following:\n#\n# bazel mod show_repo 'protobuf'\n###############################################################################\n\nload(\"@rules_rust//cargo:defs.bzl\", \"cargo_toml_env_vars\")\n\nload(\"@rules_rust//rust:defs.bzl\", \"rust_library\")\n\n# buildifier: disable=bzl-visibility\nload(\"@rules_rust//crate_universe/private:selects.bzl\", \"selects\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\ncargo_toml_env_vars(\n name = \"cargo_toml_env_vars\",\n src = \"Cargo.toml\",\n)\n\nrust_library(\n name = \"unicode_ident\",\n compile_data = glob(\n allow_empty = True,\n include = [\"**\"],\n exclude = [\n \"**/* *\",\n \".tmp_git_root/**/*\",\n \"BUILD\",\n \"BUILD.bazel\",\n \"WORKSPACE\",\n \"WORKSPACE.bazel\",\n ],\n ),\n crate_root = \"src/lib.rs\",\n edition = \"2021\",\n rustc_env_files = [\n \":cargo_toml_env_vars\",\n ],\n rustc_flags = [\n \"--cap-lints=allow\",\n ],\n srcs = glob(\n allow_empty = True,\n include = [\"**/*.rs\"],\n ),\n tags = [\n \"cargo-bazel\",\n \"crate-name=unicode-ident\",\n \"manual\",\n \"noclippy\",\n \"norustfmt\",\n ],\n target_compatible_with = select({\n \"@rules_rust//rust/platform:aarch64-apple-darwin\": [],\n \"@rules_rust//rust/platform:aarch64-unknown-linux-gnu\": [],\n \"@rules_rust//rust/platform:wasm32-unknown-unknown\": [],\n \"@rules_rust//rust/platform:wasm32-wasip1\": [],\n \"@rules_rust//rust/platform:x86_64-pc-windows-msvc\": [],\n \"@rules_rust//rust/platform:x86_64-unknown-linux-gnu\": [],\n \"@rules_rust//rust/platform:x86_64-unknown-nixos-gnu\": [],\n \"//conditions:default\": [\"@platforms//:incompatible\"],\n }),\n version = \"1.0.27\",\n)\n" + } + } + }, + "recordedRepoMappingEntries": [ + [ + "bazel_features+", + "bazel_features_globals", + "bazel_features++version_extension+bazel_features_globals" + ], + [ + "bazel_features+", + "bazel_features_version", + "bazel_features++version_extension+bazel_features_version" + ], + [ + "rules_cc+", + "bazel_tools", + "bazel_tools" + ], + [ + "rules_cc+", + "cc_compatibility_proxy", + "rules_cc++compatibility_proxy+cc_compatibility_proxy" + ], + [ + "rules_cc+", + "rules_cc", + "rules_cc+" + ], + [ + "rules_cc++compatibility_proxy+cc_compatibility_proxy", + "rules_cc", + "rules_cc+" + ], + [ + "rules_rust+", + "bazel_features", + "bazel_features+" + ], + [ + "rules_rust+", + "bazel_skylib", + "bazel_skylib+" + ], + [ + "rules_rust+", + "bazel_tools", + "bazel_tools" + ], + [ + "rules_rust+", + "rules_cc", + "rules_cc+" + ], + [ + "rules_rust+", + "rules_rust", + "rules_rust+" + ] + ] + } + }, "@@rules_rust+//crate_universe:extensions.bzl%crate": { "general": { "bzlTransitiveDigest": "KrFTrRZRbh5NHaKVHIMLnFnjiN1vwGrCVr5IcBFyfFU=", diff --git a/shared/aligned/BUILD.bazel b/shared/aligned/BUILD.bazel new file mode 100644 index 00000000..25d43a9e --- /dev/null +++ b/shared/aligned/BUILD.bazel @@ -0,0 +1,44 @@ +# Copyright 2026 Google LLC +# +# 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. + +load("@rules_rust//rust:defs.bzl", "rust_doc_test", "rust_library", "rust_test") + +rust_library( + name = "aligned", + srcs = [ + "lib.rs", + "u64.rs", + ], + crate_features = [ + "ufmt", + ], + crate_name = "openprot_aligned", + edition = "2024", + visibility = ["//visibility:public"], + deps = [ + "//third_party:ufmt", + "//third_party:zerocopy", + ], +) + +rust_test( + name = "aligned_test", + crate = ":aligned", + edition = "2024", +) + +rust_doc_test( + name = "aligned_doctest", + crate = ":aligned", +) diff --git a/shared/aligned/lib.rs b/shared/aligned/lib.rs new file mode 100644 index 00000000..ba81b990 --- /dev/null +++ b/shared/aligned/lib.rs @@ -0,0 +1,19 @@ +// Copyright 2026 Google LLC +// +// 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. + +#![cfg_attr(not(test), no_std)] + +mod u64; + +pub use u64::U64Align4; diff --git a/shared/aligned/u64.rs b/shared/aligned/u64.rs new file mode 100644 index 00000000..ce05bcb8 --- /dev/null +++ b/shared/aligned/u64.rs @@ -0,0 +1,197 @@ +// Copyright 2026 Google LLC +// +// 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. + +use zerocopy::FromBytes; +use zerocopy::Immutable; +use zerocopy::IntoBytes; +use zerocopy::KnownLayout; + +/// A 64-bit integer type that is represented as 2 32-bit words; useful +/// on 32-bit microcontrollers that can't efficiently read unaligned 32-bit +/// words. +#[derive(Clone, Copy, Default, Eq, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct U64Align4 { + pub low: u32, + pub high: u32, +} + +impl U64Align4 { + pub const fn new(val: u64) -> Self { + Self { + low: val as u32, + high: (val >> 32) as u32, + } + } + + pub const fn get(self) -> u64 { + self.low as u64 | ((self.high as u64) << 32) + } +} + +impl From for u64 { + fn from(value: U64Align4) -> Self { + value.get() + } +} + +impl From for U64Align4 { + fn from(value: u64) -> Self { + Self::new(value) + } +} + +impl core::fmt::Display for U64Align4 { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + core::fmt::Display::fmt(&u64::from(*self), f) + } +} + +impl core::fmt::LowerHex for U64Align4 { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + core::fmt::LowerHex::fmt(&u64::from(*self), f) + } +} + +impl core::fmt::Debug for U64Align4 { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + core::fmt::Debug::fmt(&u64::from(*self), f) + } +} + +#[cfg(feature = "ufmt")] +impl ufmt::uDisplay for U64Align4 { + fn fmt( + &self, + f: &mut ufmt::Formatter<'_, W>, + ) -> Result<(), W::Error> { + ufmt::uDisplay::fmt(&u64::from(*self), f) + } +} + +#[cfg(feature = "ufmt")] +impl ufmt::uDisplayHex for U64Align4 { + fn fmt_hex( + &self, + f: &mut ufmt::Formatter<'_, W>, + options: ufmt::HexOptions, + ) -> Result<(), W::Error> { + ufmt::uDisplayHex::fmt_hex(&u64::from(*self), f, options) + } +} + +#[cfg(feature = "ufmt")] +impl ufmt::uDebug for U64Align4 { + fn fmt( + &self, + f: &mut ufmt::Formatter<'_, W>, + ) -> Result<(), W::Error> { + ufmt::uDebug::fmt(&u64::from(*self), f) + } +} + +#[cfg(test)] +mod test { + #[cfg(feature = "ufmt")] + use ufmt::uwrite; + + use super::*; + + #[cfg(feature = "ufmt")] + struct Buffer { + buf: [u8; N], + len: usize, + } + + #[cfg(feature = "ufmt")] + impl Buffer { + fn new() -> Self { + Self { + buf: [0; N], + len: 0, + } + } + fn as_bytes(&self) -> &[u8] { + &self.buf[..self.len] + } + } + + #[cfg(feature = "ufmt")] + impl ufmt::uWrite for Buffer { + type Error = (); + fn write_str(&mut self, s: &str) -> Result<(), Self::Error> { + let bytes = s.as_bytes(); + if self.len + bytes.len() > N { + return Err(()); + } + self.buf[self.len..self.len + bytes.len()].copy_from_slice(bytes); + self.len += bytes.len(); + Ok(()) + } + } + + #[test] + pub fn test_unaligned_u64() { + assert_eq!( + U64Align4::new(0xcd65_0941_8884_3461), + U64Align4 { + low: 0x8884_3461, + high: 0xcd65_0941, + } + ); + assert_eq!( + U64Align4 { + low: 0x8884_3461, + high: 0xcd65_0941, + } + .get(), + 0xcd65_0941_8884_3461 + ); + assert_eq!( + u64::from(U64Align4::new(0xabbc_3d97_ecfc_8aae)), + 0xabbc_3d97_ecfc_8aae, + ); + assert_eq!( + U64Align4::from(0xd172_0953_6a66_d89e), + U64Align4::new(0xd172_0953_6a66_d89e), + ); + assert_eq!( + format!("{}", U64Align4::new(15092135555144013982)), + "15092135555144013982", + ); + assert_eq!( + format!("{:?}", U64Align4::new(15092135555144013982)), + "15092135555144013982", + ); + assert_eq!( + format!("{:x}", U64Align4::new(0xd17209536a66d89e)), + "d17209536a66d89e", + ); + + #[cfg(feature = "ufmt")] + { + let mut buf = Buffer::<32>::new(); + uwrite!(&mut buf, "{}", U64Align4::new(15092135555144013982)).unwrap(); + assert_eq!(buf.as_bytes(), b"15092135555144013982"); + + let mut buf = Buffer::<32>::new(); + uwrite!(&mut buf, "{:?}", U64Align4::new(15092135555144013982)).unwrap(); + assert_eq!(buf.as_bytes(), b"15092135555144013982"); + + let mut buf = Buffer::<32>::new(); + uwrite!(&mut buf, "{:x}", U64Align4::new(15092135555144013982)).unwrap(); + assert_eq!(buf.as_bytes(), b"d17209536a66d89e"); + } + } +} diff --git a/shared/crypto/BUILD.bazel b/shared/crypto/BUILD.bazel new file mode 100644 index 00000000..977659f8 --- /dev/null +++ b/shared/crypto/BUILD.bazel @@ -0,0 +1,38 @@ +# Copyright 2026 Google LLC +# +# 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. + +load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") + +rust_library( + name = "sha256_sw", + srcs = [ + "sha256_sw.rs", + ], + crate_name = "openprot_sha256_sw", + edition = "2024", + visibility = ["//visibility:public"], + deps = [ + "//third_party:zerocopy", + ], +) + +rust_test( + name = "sha256_sw_test", + crate = ":sha256_sw", + edition = "2024", + rustc_flags = [ + "-C", + "debug-assertions", + ], +) diff --git a/shared/crypto/sha256_sw.rs b/shared/crypto/sha256_sw.rs new file mode 100644 index 00000000..c845c187 --- /dev/null +++ b/shared/crypto/sha256_sw.rs @@ -0,0 +1,356 @@ +// Copyright 2026 Google LLC +// +// 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. + +// Based on https://chromium.googlesource.com/chromiumos/third_party/cryptoc/+/refs/heads/main/sha256.c + +#![no_std] + +static K: [u32; 64] = [ + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, + 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, + 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, + 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, + 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, + 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, + 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2, +]; + +const BLOCK_SIZE_BYTES: usize = 64; +const BLOCK_SIZE_WORDS: usize = BLOCK_SIZE_BYTES / 4; + +/// Sets byte `i` of `block`, viewed as native-endian bytes. +const fn set_block_byte(block: &mut [u32; BLOCK_SIZE_WORDS], i: usize, byte: u8) { + let mut word = block[i / 4].to_ne_bytes(); + word[i % 4] = byte; + block[i / 4] = u32::from_ne_bytes(word); +} + +pub struct Sha256 { + state: [u32; 8], + block: [u32; BLOCK_SIZE_WORDS], + count: u64, +} +impl Sha256 { + pub const fn new() -> Self { + Self { + state: [ + 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, + 0x5be0cd19, + ], + block: [0; 16], + count: 0, + } + } + pub const fn update(&mut self, data: &[u8]) { + let mut i = (self.count as usize) % BLOCK_SIZE_BYTES; + + self.count += data.len() as u64; + + let mut src_index = 0; + while src_index < data.len() { + set_block_byte(&mut self.block, i, data[src_index]); + i += 1; + src_index += 1; + if i == 64 { + self.transform(); + i = 0; + } + } + } + + /// Finalize the digest. Returns the little-endian form of the digest as a word array. + /// For example, the digest `e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855` + /// becomes `[0x7852b855, 0xa495991b, 0x649b934c, 0x27ae41e4, 0x996fb924, 0x9afbf4c8, 0x98fc1c14, 0xe3b0c442]` + pub const fn finalize_words_le(mut self) -> [u32; 8] { + self.finalize_block(); + let mut result = [0_u32; 8]; + let mut t = 0; + while t < 8 { + result[7 - t] = self.state[t]; + t += 1; + } + result + } + + /// Finalize the digest. Returns the result in the commonly-used big-endian order. + pub const fn finalize(mut self) -> [u8; 32] { + self.finalize_block(); + let mut result = [0_u32; 8]; + let mut t = 0; + while t < 8 { + result[t] = self.state[t].to_be(); + t += 1; + } + zerocopy::transmute!(result) + } + + const fn finalize_block(&mut self) { + let bit_count: u64 = self.count * 8; + + // Write byte indicating end of message + self.update(&[0x80]); + + let bytes_in_block = (self.count as usize) % BLOCK_SIZE_BYTES; + if bytes_in_block > BLOCK_SIZE_BYTES - 8 { + // There is no space for the 64-bit bit-count in this block; pad it out and start a new one + self.pad_remaining_bytes_in_block(); + self.transform(); + } + + self.pad_remaining_bytes_in_block(); + self.block[14] = ((bit_count >> 32) as u32).to_be(); + self.block[15] = (bit_count as u32).to_be(); + self.transform(); + } + const fn pad_remaining_bytes_in_block(&mut self) { + let bytes_in_block = (self.count as usize) % BLOCK_SIZE_BYTES; + let mut t = bytes_in_block; + while t < BLOCK_SIZE_BYTES { + set_block_byte(&mut self.block, t, 0); + t += 1; + } + self.count += (BLOCK_SIZE_BYTES - bytes_in_block) as u64; + } + + const fn transform(&mut self) { + let ror = u32::rotate_right; + const fn shr(a: u32, b: u32) -> u32 { + a >> b + } + + let mut w = [0_u32; 64]; + let mut t = 0; + while t < 16 { + w[t] = u32::from_be(self.block[t]); + t += 1; + } + while t < 64 { + let s0 = ror(w[t - 15], 7) ^ ror(w[t - 15], 18) ^ shr(w[t - 15], 3); + let s1 = ror(w[t - 2], 17) ^ ror(w[t - 2], 19) ^ shr(w[t - 2], 10); + w[t] = w[t - 16] + .wrapping_add(s0) + .wrapping_add(w[t - 7]) + .wrapping_add(s1); + t += 1; + } + let mut a = self.state[0]; + let mut b = self.state[1]; + let mut c = self.state[2]; + let mut d = self.state[3]; + let mut e = self.state[4]; + let mut f = self.state[5]; + let mut g = self.state[6]; + let mut h = self.state[7]; + + let mut t = 0; + while t < 64 { + let s0 = ror(a, 2) ^ ror(a, 13) ^ ror(a, 22); + let maj = (a & b) ^ (a & c) ^ (b & c); + let t2 = s0.wrapping_add(maj); + let s1 = ror(e, 6) ^ ror(e, 11) ^ ror(e, 25); + let ch = (e & f) ^ ((!e) & g); + let t1 = h + .wrapping_add(s1) + .wrapping_add(ch) + .wrapping_add(K[t]) + .wrapping_add(w[t]); + h = g; + g = f; + f = e; + e = d.wrapping_add(t1); + d = c; + c = b; + b = a; + a = t1.wrapping_add(t2); + t += 1; + } + + self.state[0] = self.state[0].wrapping_add(a); + self.state[1] = self.state[1].wrapping_add(b); + self.state[2] = self.state[2].wrapping_add(c); + self.state[3] = self.state[3].wrapping_add(d); + self.state[4] = self.state[4].wrapping_add(e); + self.state[5] = self.state[5].wrapping_add(f); + self.state[6] = self.state[6].wrapping_add(g); + self.state[7] = self.state[7].wrapping_add(h); + } +} +impl Default for Sha256 { + fn default() -> Self { + Self::new() + } +} + +pub const fn sha256(data: &[u8]) -> [u8; 32] { + let mut sha = Sha256::new(); + sha.update(data); + sha.finalize() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + pub fn smoke_test() { + assert_eq!( + sha256(b""), + [ + 0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14, 0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f, + 0xb9, 0x24, 0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c, 0xa4, 0x95, 0x99, 0x1b, + 0x78, 0x52, 0xb8, 0x55, + ] + ); + assert_eq!( + sha256(b" "), + [ + 0x36, 0xa9, 0xe7, 0xf1, 0xc9, 0x5b, 0x82, 0xff, 0xb9, 0x97, 0x43, 0xe0, 0xc5, 0xc4, + 0xce, 0x95, 0xd8, 0x3c, 0x9a, 0x43, 0x0a, 0xac, 0x59, 0xf8, 0x4e, 0xf3, 0xcb, 0xfa, + 0xb6, 0x14, 0x50, 0x68, + ] + ); + assert_eq!( + sha256(b"Hello World!"), + [ + 0x7f, 0x83, 0xb1, 0x65, 0x7f, 0xf1, 0xfc, 0x53, 0xb9, 0x2d, 0xc1, 0x81, 0x48, 0xa1, + 0xd6, 0x5d, 0xfc, 0x2d, 0x4b, 0x1f, 0xa3, 0xd6, 0x77, 0x28, 0x4a, 0xdd, 0xd2, 0x00, + 0x12, 0x6d, 0x90, 0x69, + ], + ); + // length 16 + assert_eq!( + sha256(b"Ys2e4brsOcz0fIZu"), + [ + 0xfb, 0xcd, 0x11, 0x34, 0x82, 0xf6, 0x2e, 0x10, 0x6c, 0xe6, 0x29, 0x89, 0x99, 0xeb, + 0x18, 0x8d, 0x92, 0x20, 0x69, 0xe1, 0x56, 0xb0, 0x64, 0xeb, 0xd5, 0x21, 0x65, 0xed, + 0xe2, 0xfc, 0x23, 0x09, + ] + ); + + // length 31 + assert_eq!( + sha256(b"RAS4uaNDbAs9TlKRTj2M7g4tyCzcgZL"), + [ + 0x93, 0xde, 0x27, 0xca, 0x5d, 0x8f, 0x01, 0x9b, 0x29, 0x41, 0x33, 0xfa, 0x02, 0x76, + 0x99, 0x58, 0x8a, 0xb7, 0xec, 0x2b, 0x74, 0x2e, 0x47, 0x0a, 0x40, 0x93, 0x7b, 0x24, + 0x8b, 0x24, 0x02, 0x66, + ] + ); + + // length 32 + assert_eq!( + sha256(b"ir41RSmtB3jDFJyHzOUlE0fug763ZRDh"), + [ + 0xee, 0xd7, 0x44, 0x98, 0x05, 0x4d, 0x7c, 0x35, 0xfb, 0x7d, 0x16, 0x02, 0x81, 0x8b, + 0x84, 0x53, 0xa0, 0x30, 0xb2, 0xc2, 0x7c, 0x4a, 0x44, 0xbd, 0x77, 0xbe, 0xbb, 0x26, + 0x57, 0x16, 0x27, 0xa0 + ], + ); + // length 55 + assert_eq!( + b"iZOB3ptdlWmgVdSYIWOxgR1M3NmJIDE8Uu54SsKsX+1anla5q82mg7a".len(), + 55 + ); + assert_eq!( + sha256(b"iZOB3ptdlWmgVdSYIWOxgR1M3NmJIDE8Uu54SsKsX81anla5q82mg7a"), + [ + 0x78, 0x18, 0x60, 0xe5, 0x5e, 0x5e, 0x23, 0x1e, 0x5d, 0xda, 0x2a, 0xdd, 0xa5, 0xee, + 0x6d, 0x95, 0x84, 0xac, 0x0c, 0xf3, 0xec, 0x09, 0xd2, 0x2b, 0x4a, 0x13, 0xcf, 0x0e, + 0x10, 0xd3, 0x06, 0xef, + ] + ); + + // length 56 (fits in exactly one block) + assert_eq!( + sha256(b"7OJe1chgSma4phwrwoiRV8UXMVuq9q2zUeqQ21KUZHEUnY7FUdj29xke"), + [ + 0x9e, 0x52, 0x75, 0x5c, 0x0d, 0x12, 0x18, 0x65, 0x83, 0x4e, 0x97, 0xfe, 0x13, 0x1c, + 0x00, 0x1f, 0xef, 0x28, 0xd7, 0x37, 0xb3, 0x47, 0x54, 0xdd, 0x44, 0x31, 0x09, 0xd5, + 0x74, 0xce, 0x61, 0x7f, + ] + ); + + // length 57 (expands to two blocks) + assert_eq!( + sha256(b"Z6A0YmIVthygYJjbTWB3DjJCOVcf9nycCH0e5oCBlTPFysnj1XnhEdP3F"), + [ + 0xd5, 0xde, 0xcc, 0x3e, 0x60, 0xbe, 0xfe, 0x45, 0xe8, 0x1a, 0xf1, 0xad, 0x11, 0x7c, + 0xff, 0x97, 0x5d, 0xb8, 0x58, 0xf8, 0x4c, 0xcf, 0x8a, 0x15, 0x2c, 0x35, 0x51, 0x49, + 0x72, 0xff, 0x45, 0x1f, + ] + ); + + // length 63 + assert_eq!( + sha256(b"k3uIvGDoDYximAaerV1IxNUJczk8Dg7aGC6cxh3z2e3otSwhOZQhAdlSjLovgxc"), + [ + 0x15, 0xfb, 0xb2, 0x73, 0x73, 0xdb, 0xaa, 0x4a, 0xcc, 0x32, 0x77, 0x36, 0x36, 0xf3, + 0xad, 0xab, 0x95, 0xc4, 0xaa, 0xcc, 0x65, 0x77, 0x81, 0xa4, 0x3c, 0x38, 0x64, 0x12, + 0x37, 0x11, 0xce, 0xee, + ] + ); + + // length 64 + assert_eq!( + sha256(b"HizlgtPfn57DwXecYifNBO9wL7hGGSdCrIPe3zWpmIPyBF0xpkS8ThN1nsXmvztQ"), + [ + 0x9c, 0x66, 0x96, 0xd5, 0x81, 0xfc, 0x4c, 0x60, 0xa5, 0x39, 0x9b, 0xd6, 0xa8, 0x19, + 0xca, 0xcd, 0xc1, 0xf7, 0x92, 0xc2, 0x55, 0x84, 0x3f, 0xbe, 0x4b, 0x20, 0xa2, 0x0f, + 0xa5, 0xbd, 0x16, 0x22, + ] + ); + // length 65 + assert_eq!( + sha256(b"lTVBIeYnVtawL4VVe3NXXIyLGd8s4PPMcfOnijH9HSluYojeueNPtcrormvbp0H70"), + [ + 0xa3, 0xe1, 0xdb, 0xbb, 0xdc, 0xa8, 0x82, 0xe9, 0x0a, 0xf0, 0xcf, 0xf0, 0x7c, 0xba, + 0x36, 0xf3, 0x08, 0x54, 0x32, 0x41, 0x60, 0x1c, 0xe3, 0xcb, 0xfd, 0x19, 0xf6, 0x0e, + 0x93, 0xed, 0x41, 0xd4, + ] + ); + // length 120 + assert_eq!( + sha256(b"R7ncj4usgKjwECA5WkHHkOxtasOj8oPKPVJjo1Y326jLilcgg702uSiD9zz3K0XXMzGh88MDfUhCgbQgQRpPx2FFzqXSmDR5CfS1VOkk7wZuDz6JwTV44US2"), + [ + 0x95, 0x10, 0xe5, 0x70, 0x38, 0xd4, 0xb1, 0x52, 0x82, 0x06, + 0x8d, 0x0c, 0x2d, 0x5f, 0xe9, 0x18, 0xf9, 0xcf, 0xc3, 0xa5, + 0x74, 0x22, 0x95, 0x9b, 0x42, 0x90, 0xc6, 0x5b, 0x32, 0xc5, + 0x02, 0x24 + ] + ); + // length 121 (3 blocks) + assert_eq!( + sha256(b"2oRV9iFl4FE1hiEkIjATkgMJJbWNMUWHoLB8x603j86Gsy1SqLRVQ0uTFydUvrJrWfItaS0ulIano9mifBWRVNv4GhsPIpYnYJFbSL4m1h83OAzpHhULf90xO"), + [ + 0x0f, 0xa0, 0xd2, 0xef, 0x94, 0x38, 0x87, 0x80, 0x92, 0xc4, + 0x1c, 0x60, 0xc5, 0x9d, 0xcb, 0xe9, 0x32, 0xcb, 0x2d, 0x8b, + 0x3a, 0x52, 0x7a, 0xd4, 0x67, 0xb4, 0xfa, 0x8f, 0x96, 0x84, + 0x36, 0x4a, + ] + ) + } + #[test] + pub fn test_words_le() { + let mut sha = Sha256::new(); + sha.update(b""); + assert_eq!( + sha.finalize_words_le(), + [ + 0x7852b855, 0xa495991b, 0x649b934c, 0x27ae41e4, 0x996fb924, 0x9afbf4c8, 0x98fc1c14, + 0xe3b0c442, + ] + ); + } +} diff --git a/shared/fingerprints/BUILD.bazel b/shared/fingerprints/BUILD.bazel new file mode 100644 index 00000000..40b35cb8 --- /dev/null +++ b/shared/fingerprints/BUILD.bazel @@ -0,0 +1,39 @@ +# Copyright 2026 Google LLC +# +# 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. + +load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") + +package(default_visibility = ["//visibility:public"]) + +rust_library( + name = "fingerprints", + srcs = [ + "lib.rs", + ], + crate_name = "openprot_fingerprints", + edition = "2024", + visibility = ["//visibility:public"], + deps = [ + "//shared/crypto:sha256_sw", + "//shared/openprot/proto:crypto_tlv", + "//shared/tlv", + "//third_party:zerocopy", + ], +) + +rust_test( + name = "fingerprints_test", + crate = ":fingerprints", + edition = "2024", +) diff --git a/shared/fingerprints/lib.rs b/shared/fingerprints/lib.rs new file mode 100644 index 00000000..49d6fce1 --- /dev/null +++ b/shared/fingerprints/lib.rs @@ -0,0 +1,610 @@ +// Copyright 2026 Google LLC +// +// 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. + +#![cfg_attr(not(test), no_std)] + +use core::hint::black_box; +use crypto_tlv::HybridP256MlDsa44FingerprintDigest; +use crypto_tlv::HybridP256MlDsa44FingerprintExt; +use crypto_tlv::HybridP256MlDsa44PublicMaterial; +use crypto_tlv::HybridP256SlhDsaFingerprintDigest; +use crypto_tlv::HybridP256SlhDsaFingerprintExt; +use crypto_tlv::HybridP256SlhDsaPublicMaterial; +use crypto_tlv::KeyDomain; +use crypto_tlv::KeyFingerprint; +use crypto_tlv::KeyFingerprintExt; +use crypto_tlv::KeyFingerprintKeyType; +use crypto_tlv::MlDsa44FingerprintDigest; +use crypto_tlv::MlDsa44FingerprintExt; +use crypto_tlv::MlDsa44PublicMaterial; +use crypto_tlv::P256FingerprintDigest; +use crypto_tlv::P256FingerprintExt; +use crypto_tlv::P256PublicMaterial; +use crypto_tlv::PublicKey; +use crypto_tlv::PublicKeyExt; +use crypto_tlv::PublicKeyKeyMaterial; +use crypto_tlv::Rsa2048FingerprintDigest; +use crypto_tlv::Rsa2048FingerprintExt; +use crypto_tlv::Rsa2048PublicMaterial; +use crypto_tlv::Rsa3072FingerprintDigest; +use crypto_tlv::Rsa3072FingerprintExt; +use crypto_tlv::Rsa3072PublicMaterial; +use crypto_tlv::Rsa4096FingerprintDigest; +use crypto_tlv::Rsa4096FingerprintExt; +use crypto_tlv::Rsa4096PublicMaterial; +use crypto_tlv::Sha2_256Digest; +use crypto_tlv::SlhDsaFingerprintDigest; +use crypto_tlv::SlhDsaFingerprintExt; +use crypto_tlv::SlhDsaPublicMaterial; +use openprot_sha256_sw::Sha256; +use zerocopy::IntoBytes; + +// ============================================================================= +// Fingerprintable Key Trait & Implementations +// ============================================================================= + +/// Trait for cryptographic keys that can compute a canonical SHA-256 fingerprint. +pub trait FingerprintableKey { + /// Feeds the canonical byte representation of the public key material into a SHA-256 hasher. + fn update_hasher(&self, hasher: &mut Sha256); + + /// Computes the 32-byte SHA-256 fingerprint digest of this key. + fn fingerprint_sha256(&self) -> [u8; 32] { + let mut hasher = Sha256::new(); + self.update_hasher(&mut hasher); + hasher.finalize() + } +} + +impl FingerprintableKey for P256PublicMaterial { + fn update_hasher(&self, hasher: &mut Sha256) { + hasher.update(self.x.as_bytes()); + hasher.update(self.y.as_bytes()); + } +} + +impl FingerprintableKey for SlhDsaPublicMaterial { + fn update_hasher(&self, hasher: &mut Sha256) { + hasher.update(self.slhdsa.as_bytes()); + } +} + +impl FingerprintableKey for MlDsa44PublicMaterial { + fn update_hasher(&self, hasher: &mut Sha256) { + hasher.update(&self.mldsa); + } +} + +impl FingerprintableKey for HybridP256MlDsa44PublicMaterial { + fn update_hasher(&self, hasher: &mut Sha256) { + hasher.update(self.x.as_bytes()); + hasher.update(self.y.as_bytes()); + hasher.update(&self.mldsa); + } +} + +impl FingerprintableKey for HybridP256SlhDsaPublicMaterial { + fn update_hasher(&self, hasher: &mut Sha256) { + hasher.update(self.x.as_bytes()); + hasher.update(self.y.as_bytes()); + hasher.update(self.slhdsa.as_bytes()); + } +} + +impl FingerprintableKey for Rsa2048PublicMaterial { + fn update_hasher(&self, hasher: &mut Sha256) { + hasher.update(&self.modulus); + } +} + +impl FingerprintableKey for Rsa3072PublicMaterial { + fn update_hasher(&self, hasher: &mut Sha256) { + hasher.update(&self.modulus); + } +} + +impl FingerprintableKey for Rsa4096PublicMaterial { + fn update_hasher(&self, hasher: &mut Sha256) { + hasher.update(&self.modulus); + } +} + +impl<'a> FingerprintableKey for PublicKeyKeyMaterial<'a> { + fn update_hasher(&self, hasher: &mut Sha256) { + match self { + PublicKeyKeyMaterial::P256(item) => item.data.update_hasher(hasher), + PublicKeyKeyMaterial::Slhdsa(item) => item.data.update_hasher(hasher), + PublicKeyKeyMaterial::Mldsa(item) => item.data.update_hasher(hasher), + PublicKeyKeyMaterial::HybridP256Slhdsa(item) => item.data.update_hasher(hasher), + PublicKeyKeyMaterial::HybridP256Mldsa(item) => item.data.update_hasher(hasher), + PublicKeyKeyMaterial::Rsa2048(item) => item.data.update_hasher(hasher), + PublicKeyKeyMaterial::Rsa3072(item) => item.data.update_hasher(hasher), + PublicKeyKeyMaterial::Rsa4096(item) => item.data.update_hasher(hasher), + } + } +} + +impl FingerprintableKey for &T { + fn update_hasher(&self, hasher: &mut Sha256) { + (*self).update_hasher(hasher); + } +} + +// ============================================================================= +// Constant-Time Comparison Helpers +// ============================================================================= + +/// Constant-time comparison for 32-byte arrays to prevent timing attacks. +#[inline(never)] +pub fn constant_time_eq_32(a: &[u8; 32], b: &[u8; 32]) -> bool { + let mut diff = 0u8; + for i in 0..32 { + diff |= a[i] ^ b[i]; + } + black_box(diff) == 0 +} + +// ============================================================================= +// Verification Functions +// ============================================================================= + +/// Verifies whether a given public key material and domain match the specified `KeyFingerprint` TLV item. +pub fn verify_key_matches_fingerprint( + public_key: &PublicKeyKeyMaterial<'_>, + key_domain: KeyDomain, + fingerprint: &openprot_tlv::TlvItem<'_, KeyFingerprint>, +) -> bool { + if key_domain != fingerprint.data.key_domain { + return false; + } + + let Some(fp_key_type) = fingerprint.key_type() else { + return false; + }; + + match (public_key, fp_key_type) { + (PublicKeyKeyMaterial::P256(pk), KeyFingerprintKeyType::P256(fp)) => { + matches!(fp.digest(), Some(P256FingerprintDigest::Sha2256(d)) if constant_time_eq_32(&pk.data.fingerprint_sha256(), &d.data.data)) + } + (PublicKeyKeyMaterial::Slhdsa(pk), KeyFingerprintKeyType::Slhdsa(fp)) => { + matches!(fp.digest(), Some(SlhDsaFingerprintDigest::Sha2256(d)) if constant_time_eq_32(&pk.data.fingerprint_sha256(), &d.data.data)) + } + (PublicKeyKeyMaterial::Mldsa(pk), KeyFingerprintKeyType::Mldsa(fp)) => { + matches!(fp.digest(), Some(MlDsa44FingerprintDigest::Sha2256(d)) if constant_time_eq_32(&pk.data.fingerprint_sha256(), &d.data.data)) + } + (PublicKeyKeyMaterial::HybridP256Mldsa(pk), KeyFingerprintKeyType::HybridP256Mldsa(fp)) => { + matches!(fp.digest(), Some(HybridP256MlDsa44FingerprintDigest::Sha2256(d)) if constant_time_eq_32(&pk.data.fingerprint_sha256(), &d.data.data)) + } + ( + PublicKeyKeyMaterial::HybridP256Slhdsa(pk), + KeyFingerprintKeyType::HybridP256Slhdsa(fp), + ) => { + matches!(fp.digest(), Some(HybridP256SlhDsaFingerprintDigest::Sha2256(d)) if constant_time_eq_32(&pk.data.fingerprint_sha256(), &d.data.data)) + } + (PublicKeyKeyMaterial::Rsa2048(pk), KeyFingerprintKeyType::Rsa2048(fp)) => { + matches!(fp.digest(), Some(Rsa2048FingerprintDigest::Sha2256(d)) if constant_time_eq_32(&pk.data.fingerprint_sha256(), &d.data.data)) + } + (PublicKeyKeyMaterial::Rsa3072(pk), KeyFingerprintKeyType::Rsa3072(fp)) => { + matches!(fp.digest(), Some(Rsa3072FingerprintDigest::Sha2256(d)) if constant_time_eq_32(&pk.data.fingerprint_sha256(), &d.data.data)) + } + (PublicKeyKeyMaterial::Rsa4096(pk), KeyFingerprintKeyType::Rsa4096(fp)) => { + matches!(fp.digest(), Some(Rsa4096FingerprintDigest::Sha2256(d)) if constant_time_eq_32(&pk.data.fingerprint_sha256(), &d.data.data)) + } + _ => false, + } +} + +/// Convenience helper to verify a parsed `PublicKey` TLV item against a `KeyFingerprint` TLV item. +pub fn verify_public_key_matches_fingerprint( + public_key: &openprot_tlv::TlvItem<'_, PublicKey>, + fingerprint: &openprot_tlv::TlvItem<'_, KeyFingerprint>, +) -> bool { + let Some(km) = public_key.key_material() else { + return false; + }; + verify_key_matches_fingerprint(&km, public_key.data.key_domain, fingerprint) +} + +/// Constant-time helper to check if a 32-byte digest matches a `Sha2_256Digest` TLV item. +pub fn verify_digest_matches_sha256( + digest: &[u8; 32], + expected: &openprot_tlv::TlvItem<'_, Sha2_256Digest>, +) -> bool { + constant_time_eq_32(digest, &expected.data.data) +} + +// ============================================================================= +// Tests +// ============================================================================= + +#[cfg(test)] +mod tests { + use super::*; + use crypto_tlv::HybridP256MlDsa44Fingerprint; + use crypto_tlv::HybridP256SlhDsaFingerprint; + use crypto_tlv::MlDsa44Fingerprint; + use crypto_tlv::P256Fingerprint; + use crypto_tlv::Rsa3072Fingerprint; + use crypto_tlv::SlhDsaFingerprint; + use openprot_tlv::TlvBuilder; + use openprot_tlv::TlvData; + + #[test] + fn test_fingerprintable_keys() { + let p256 = P256PublicMaterial { + x: [1, 2, 3, 4, 5, 6, 7, 8], + y: [9, 10, 11, 12, 13, 14, 15, 16], + }; + let mut exp_p256 = Sha256::new(); + exp_p256.update(p256.x.as_bytes()); + exp_p256.update(p256.y.as_bytes()); + assert_eq!(p256.fingerprint_sha256(), exp_p256.finalize()); + + let slhdsa = SlhDsaPublicMaterial { slhdsa: [0x55; 8] }; + let mut exp_slh = Sha256::new(); + exp_slh.update(slhdsa.slhdsa.as_bytes()); + assert_eq!(slhdsa.fingerprint_sha256(), exp_slh.finalize()); + + let mldsa = MlDsa44PublicMaterial { + mldsa: [0x42; 1312], + }; + let mut exp_mldsa = Sha256::new(); + exp_mldsa.update(&mldsa.mldsa); + assert_eq!(mldsa.fingerprint_sha256(), exp_mldsa.finalize()); + + let hybrid_mldsa = HybridP256MlDsa44PublicMaterial { + x: [0x11; 8], + y: [0x22; 8], + mldsa: [0x33; 1312], + }; + let mut exp_h_mldsa = Sha256::new(); + exp_h_mldsa.update(hybrid_mldsa.x.as_bytes()); + exp_h_mldsa.update(hybrid_mldsa.y.as_bytes()); + exp_h_mldsa.update(&hybrid_mldsa.mldsa); + assert_eq!(hybrid_mldsa.fingerprint_sha256(), exp_h_mldsa.finalize()); + + let hybrid_slhdsa = HybridP256SlhDsaPublicMaterial { + x: [0x11; 8], + y: [0x22; 8], + slhdsa: [0x33; 8], + }; + let mut exp_h_slhdsa = Sha256::new(); + exp_h_slhdsa.update(hybrid_slhdsa.x.as_bytes()); + exp_h_slhdsa.update(hybrid_slhdsa.y.as_bytes()); + exp_h_slhdsa.update(hybrid_slhdsa.slhdsa.as_bytes()); + assert_eq!(hybrid_slhdsa.fingerprint_sha256(), exp_h_slhdsa.finalize()); + + let rsa3072 = Rsa3072PublicMaterial { + modulus: [0x77; 384], + }; + let mut exp_rsa3072 = Sha256::new(); + exp_rsa3072.update(&rsa3072.modulus); + assert_eq!(rsa3072.fingerprint_sha256(), exp_rsa3072.finalize()); + } + + #[test] + fn test_verify_rsa3072() { + let rsa_mat = Rsa3072PublicMaterial { + modulus: [0x77; 384], + }; + + let mut kf_words = [0u32; 64]; + let (kf, mut builder) = TlvBuilder::new::(&mut kf_words).unwrap(); + kf.key_domain = KeyDomain::PAYLOAD_PROD; + builder + .add_in::(|_fp, b| { + let d = b.add::()?; + d.data = rsa_mat.fingerprint_sha256(); + Ok(()) + }) + .unwrap(); + let kf_bytes = builder.finish().into_bytes(&kf_words).unwrap(); + let fp_item = TlvData::overlay_bytes(kf_bytes) + .unwrap() + .iter::() + .next() + .unwrap(); + + let mut pk_words = [0u32; 128]; + let (pk, mut pk_b) = TlvBuilder::new::(&mut pk_words).unwrap(); + pk.key_domain = KeyDomain::PAYLOAD_PROD; + *pk_b.add::().unwrap() = rsa_mat; + let pk_bytes = pk_b.finish().into_bytes(&pk_words).unwrap(); + let pk_item = TlvData::overlay_bytes(pk_bytes) + .unwrap() + .iter::() + .next() + .unwrap(); + + assert!(verify_public_key_matches_fingerprint(&pk_item, &fp_item)); + + // A different RSA size must be rejected: the fingerprint is tagged + // RSA3, so both the key type and the digest have to line up. + let mut short_words = [0u32; 128]; + let (short_pk, mut short_b) = TlvBuilder::new::(&mut short_words).unwrap(); + short_pk.key_domain = KeyDomain::PAYLOAD_PROD; + *short_b.add::().unwrap() = Rsa2048PublicMaterial { + modulus: [0x77; 256], + }; + let short_bytes = short_b.finish().into_bytes(&short_words).unwrap(); + let short_item = TlvData::overlay_bytes(short_bytes) + .unwrap() + .iter::() + .next() + .unwrap(); + assert!(!verify_public_key_matches_fingerprint( + &short_item, + &fp_item + )); + } + + #[test] + fn test_verify_p256() { + let p256_mat = P256PublicMaterial { + x: [1, 2, 3, 4, 5, 6, 7, 8], + y: [9, 10, 11, 12, 13, 14, 15, 16], + }; + let expected = p256_mat.fingerprint_sha256(); + + let mut kf_words = [0u32; 64]; + let (kf, mut builder) = TlvBuilder::new::(&mut kf_words).unwrap(); + kf.key_domain = KeyDomain::PAYLOAD_PROD; + builder + .add_in::(|_fp, b| { + let d = b.add::()?; + d.data = expected; + Ok(()) + }) + .unwrap(); + let kf_bytes = builder.finish().into_bytes(&kf_words).unwrap(); + let fp_item = TlvData::overlay_bytes(kf_bytes) + .unwrap() + .iter::() + .next() + .unwrap(); + + let mut pk_words = [0u32; 64]; + let (pk, mut pk_b) = TlvBuilder::new::(&mut pk_words).unwrap(); + pk.key_domain = KeyDomain::PAYLOAD_PROD; + *pk_b.add::().unwrap() = p256_mat; + let pk_bytes = pk_b.finish().into_bytes(&pk_words).unwrap(); + let pk_item = TlvData::overlay_bytes(pk_bytes) + .unwrap() + .iter::() + .next() + .unwrap(); + + assert!(verify_public_key_matches_fingerprint(&pk_item, &fp_item)); + assert!(!verify_key_matches_fingerprint( + &pk_item.key_material().unwrap(), + KeyDomain::PAYLOAD_DEV, + &fp_item + )); + } + + #[test] + fn test_verify_slhdsa() { + let slhdsa_mat = SlhDsaPublicMaterial { slhdsa: [0x99; 8] }; + let expected = slhdsa_mat.fingerprint_sha256(); + + let mut kf_words = [0u32; 64]; + let (kf, mut builder) = TlvBuilder::new::(&mut kf_words).unwrap(); + kf.key_domain = KeyDomain::PAYLOAD_PROD; + builder + .add_in::(|_fp, b| { + let d = b.add::()?; + d.data = expected; + Ok(()) + }) + .unwrap(); + let kf_bytes = builder.finish().into_bytes(&kf_words).unwrap(); + let fp_item = TlvData::overlay_bytes(kf_bytes) + .unwrap() + .iter::() + .next() + .unwrap(); + + let mut pk_words = [0u32; 64]; + let (pk, mut pk_b) = TlvBuilder::new::(&mut pk_words).unwrap(); + pk.key_domain = KeyDomain::PAYLOAD_PROD; + *pk_b.add::().unwrap() = slhdsa_mat; + let pk_bytes = pk_b.finish().into_bytes(&pk_words).unwrap(); + let pk_item = TlvData::overlay_bytes(pk_bytes) + .unwrap() + .iter::() + .next() + .unwrap(); + + assert!(verify_public_key_matches_fingerprint(&pk_item, &fp_item)); + } + + #[test] + fn test_verify_hybrid_mldsa() { + let mat = HybridP256MlDsa44PublicMaterial { + x: [0x12; 8], + y: [0x34; 8], + mldsa: [0x56; 1312], + }; + let expected = mat.fingerprint_sha256(); + + let mut kf_words = [0u32; 64]; + let (kf, mut builder) = TlvBuilder::new::(&mut kf_words).unwrap(); + kf.key_domain = KeyDomain::PAYLOAD_PROD; + builder + .add_in::(|_fp, b| { + let d = b.add::()?; + d.data = expected; + Ok(()) + }) + .unwrap(); + let kf_bytes = builder.finish().into_bytes(&kf_words).unwrap(); + let fp_item = TlvData::overlay_bytes(kf_bytes) + .unwrap() + .iter::() + .next() + .unwrap(); + + let mut pk_words = [0u32; 512]; + let (pk, mut pk_b) = TlvBuilder::new::(&mut pk_words).unwrap(); + pk.key_domain = KeyDomain::PAYLOAD_PROD; + *pk_b.add::().unwrap() = mat; + let pk_bytes = pk_b.finish().into_bytes(&pk_words).unwrap(); + let pk_item = TlvData::overlay_bytes(pk_bytes) + .unwrap() + .iter::() + .next() + .unwrap(); + + assert!(verify_public_key_matches_fingerprint(&pk_item, &fp_item)); + + // Tampered key fails + let mut tampered = mat; + tampered.mldsa[0] ^= 0xff; + let mut pk_words2 = [0u32; 512]; + let (pk2, mut pk_b2) = TlvBuilder::new::(&mut pk_words2).unwrap(); + pk2.key_domain = KeyDomain::PAYLOAD_PROD; + *pk_b2.add::().unwrap() = tampered; + let pk_bytes2 = pk_b2.finish().into_bytes(&pk_words2).unwrap(); + let pk_item2 = TlvData::overlay_bytes(pk_bytes2) + .unwrap() + .iter::() + .next() + .unwrap(); + assert!(!verify_public_key_matches_fingerprint(&pk_item2, &fp_item)); + } + + #[test] + fn test_verify_hybrid_slhdsa() { + let mat = HybridP256SlhDsaPublicMaterial { + x: [0x11; 8], + y: [0x22; 8], + slhdsa: [0x33; 8], + }; + let expected = mat.fingerprint_sha256(); + + let mut kf_words = [0u32; 64]; + let (kf, mut builder) = TlvBuilder::new::(&mut kf_words).unwrap(); + kf.key_domain = KeyDomain::PAYLOAD_DEV; + builder + .add_in::(|_fp, b| { + let d = b.add::()?; + d.data = expected; + Ok(()) + }) + .unwrap(); + let kf_bytes = builder.finish().into_bytes(&kf_words).unwrap(); + let fp_item = TlvData::overlay_bytes(kf_bytes) + .unwrap() + .iter::() + .next() + .unwrap(); + + let mut pk_words = [0u32; 64]; + let (pk, mut pk_b) = TlvBuilder::new::(&mut pk_words).unwrap(); + pk.key_domain = KeyDomain::PAYLOAD_DEV; + *pk_b.add::().unwrap() = mat; + let pk_bytes = pk_b.finish().into_bytes(&pk_words).unwrap(); + let pk_item = TlvData::overlay_bytes(pk_bytes) + .unwrap() + .iter::() + .next() + .unwrap(); + + assert!(verify_public_key_matches_fingerprint(&pk_item, &fp_item)); + } + + #[test] + fn test_verify_mldsa() { + let mat = MlDsa44PublicMaterial { + mldsa: [0x78; 1312], + }; + let expected = mat.fingerprint_sha256(); + + let mut kf_words = [0u32; 64]; + let (kf, mut builder) = TlvBuilder::new::(&mut kf_words).unwrap(); + kf.key_domain = KeyDomain::PAYLOAD_TEST; + builder + .add_in::(|_fp, b| { + let d = b.add::()?; + d.data = expected; + Ok(()) + }) + .unwrap(); + let kf_bytes = builder.finish().into_bytes(&kf_words).unwrap(); + let fp_item = TlvData::overlay_bytes(kf_bytes) + .unwrap() + .iter::() + .next() + .unwrap(); + + let mut pk_words = [0u32; 512]; + let (pk, mut pk_b) = TlvBuilder::new::(&mut pk_words).unwrap(); + pk.key_domain = KeyDomain::PAYLOAD_TEST; + *pk_b.add::().unwrap() = mat; + let pk_bytes = pk_b.finish().into_bytes(&pk_words).unwrap(); + let pk_item = TlvData::overlay_bytes(pk_bytes) + .unwrap() + .iter::() + .next() + .unwrap(); + + assert!(verify_public_key_matches_fingerprint(&pk_item, &fp_item)); + + // Key type mismatch (MlDsa key checked against P256 fingerprint) + let mut p256_words = [0u32; 64]; + let (p256_kf, mut p256_b) = TlvBuilder::new::(&mut p256_words).unwrap(); + p256_kf.key_domain = KeyDomain::PAYLOAD_TEST; + p256_b + .add_in::(|_fp, b| { + let d = b.add::()?; + d.data = expected; + Ok(()) + }) + .unwrap(); + let p256_bytes = p256_b.finish().into_bytes(&p256_words).unwrap(); + let p256_fp = TlvData::overlay_bytes(p256_bytes) + .unwrap() + .iter::() + .next() + .unwrap(); + assert!(!verify_public_key_matches_fingerprint(&pk_item, &p256_fp)); + } + + #[test] + fn test_verify_digest_matches_sha256() { + let digest_data = [0x77u8; 32]; + let mut digest_words = [0u32; 16]; + let (digest_obj, builder) = TlvBuilder::new::(&mut digest_words).unwrap(); + digest_obj.data = digest_data; + let digest_bytes = builder.finish().into_bytes(&digest_words).unwrap(); + let tlv = TlvData::overlay_bytes(digest_bytes).unwrap(); + let digest_item = tlv.iter::().next().unwrap(); + + assert!(verify_digest_matches_sha256(&digest_data, &digest_item)); + + let mut tampered = digest_data; + tampered[0] ^= 0x01; + assert!(!verify_digest_matches_sha256(&tampered, &digest_item)); + } + + #[test] + fn test_constant_time_eq_32() { + let a32 = [0xaa; 32]; + let b32 = [0xaa; 32]; + let mut c32 = [0xaa; 32]; + c32[31] = 0xab; + assert!(constant_time_eq_32(&a32, &b32)); + assert!(!constant_time_eq_32(&a32, &c32)); + } +} diff --git a/shared/golden/BUILD.bazel b/shared/golden/BUILD.bazel new file mode 100644 index 00000000..dce9b1a6 --- /dev/null +++ b/shared/golden/BUILD.bazel @@ -0,0 +1,52 @@ +# Copyright 2026 Google LLC +# +# 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. + +load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_library", "rust_test") + +rust_library( + name = "golden", + testonly = True, + srcs = ["golden.rs"], + crate_name = "openprot_golden", + edition = "2024", + visibility = ["//visibility:public"], + deps = [ + "//third_party:difflib", + ], +) + +rust_binary( + name = "driver", + testonly = True, + srcs = ["driver.rs"], + edition = "2024", + visibility = ["//visibility:public"], + deps = [ + ":golden", + "//third_party:argh", + ], +) + +rust_test( + name = "golden_test", + compile_data = glob(["testdata/**"]), + crate = ":golden", + edition = "2024", +) + +rust_test( + name = "driver_test", + crate = ":driver", + edition = "2024", +) diff --git a/shared/golden/driver.rs b/shared/golden/driver.rs new file mode 100644 index 00000000..a99fadfc --- /dev/null +++ b/shared/golden/driver.rs @@ -0,0 +1,121 @@ +// Copyright 2026 Google LLC +// +// 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. + +use argh::FromArgs; +use std::path::Path; +use std::path::PathBuf; + +#[derive(FromArgs, Debug)] +/// Reusable golden file comparison driver. +struct Cli { + #[argh(option)] + /// a line prefix. If a line starts with this prefix, the remaining content + /// on that line will be redacted. Can be specified multiple times. + redact_line_prefix: Vec, + + #[argh(positional)] + /// files to check: coming in pairs: [ ]... + pairs: Vec, +} + +fn redact_content(s: &str, redact_prefixes: &[String]) -> String { + if redact_prefixes.is_empty() { + return s.to_string(); + } + + let mut result = String::with_capacity(s.len()); + + for line in s.lines() { + if let Some(prefix) = redact_prefixes + .iter() + .find(|&prefix| line.starts_with(prefix)) + { + result.push_str(prefix); + result.push_str("\n"); + } else { + result.push_str(line); + result.push('\n'); + } + } + + result +} + +fn main() { + let cli: Cli = argh::from_env(); + + if !cli.pairs.len().is_multiple_of(2) { + eprintln!("Error: Positional arguments must come in pairs (actual_path, golden_path)"); + std::process::exit(1); + } + + for [actual_path, golden_path] in cli.pairs.as_chunks::<2>().0 { + let actual_raw = std::fs::read_to_string(actual_path).unwrap_or_else(|e| { + panic!("Failed to read actual file {}: {e}", actual_path.display()) + }); + + let actual = redact_content(&actual_raw, &cli.redact_line_prefix); + + let expected = std::fs::read_to_string(golden_path).unwrap_or_default(); + + openprot_golden::assert_eq_golden_impl(&actual, &expected, Path::new(""), golden_path); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_redact_content() { + let prefixes = vec!["redact:".to_string(), "hide:".to_string()]; + + // Empty input and empty prefixes + assert_eq!(redact_content("", &[]), ""); + + // Non-empty input and empty prefixes + assert_eq!(redact_content("hello\nworld", &[]), "hello\nworld"); + + // Input with no matching prefixes + assert_eq!(redact_content("hello\nworld", &prefixes), "hello\nworld\n"); + + // Input with matching prefixes + assert_eq!( + redact_content( + "hello\nredact:secret info\nhide:private data\nkeep this", + &prefixes + ), + "hello\nredact:\nhide:\nkeep this\n" + ); + + // Line starting with a prefix vs containing prefix elsewhere + assert_eq!( + redact_content("not a redact:prefix here\nredact:at start\n", &prefixes), + "not a redact:prefix here\nredact:\n" + ); + + // Multiple prefixes matching (the first matching prefix in the list is used) + let overlapping_prefixes = vec!["pre".to_string(), "prefix".to_string()]; + assert_eq!( + redact_content("prefix:data", &overlapping_prefixes), + "pre\n" + ); + + let non_overlapping_prefixes = vec!["pre:".to_string(), "prefix:".to_string()]; + assert_eq!( + redact_content("prefix:data", &non_overlapping_prefixes), + "prefix:\n" + ); + } +} diff --git a/shared/golden/golden.rs b/shared/golden/golden.rs new file mode 100644 index 00000000..6d333ac5 --- /dev/null +++ b/shared/golden/golden.rs @@ -0,0 +1,111 @@ +// Copyright 2026 Google LLC +// +// 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. + +//! This library provides a macro for comparing arbitrary text data to a +//! golden file, and allowing a person running a test to automatically set the +//! golden data to the "actual" data by setting an environment variable when +//! running the unit test. +//! +//! This is most useful when the data being compared is large, and +//! correctness can be easily determined by having a human look at a diff. Some +//! examples are the output of an HTML template, or the output of a large +//! formatted struct. + +use std::path::Path; + +/// assert that a string matches a golden file. Example: +/// +/// ``` +/// assert_eq_golden!("foo\nbar\nbaz\n", "testdata/test1.golden") +/// ``` +#[macro_export] +macro_rules! assert_eq_golden { + ($data:expr, $golden_file:literal) => { + $crate::assert_eq_golden_impl( + $data, + include_str!($golden_file), + std::path::Path::new(file!()), + std::path::Path::new($golden_file), + ) + }; +} + +#[track_caller] +pub fn assert_eq_golden_impl(actual: &str, expected: &str, src_path: &Path, path: &Path) { + if actual == expected { + return; + } + let actual_lines: Vec<&str> = actual.split_inclusive('\n').collect(); + let expected_lines: Vec<&str> = expected.split_inclusive('\n').collect(); + let diff_lines = difflib::unified_diff( + &expected_lines, + &actual_lines, + &path.to_string_lossy(), + &path.with_extension("actual").to_string_lossy(), + "", + "", + 3, + ); + if std::env::vars_os().any(|a| a.0 == "UPDATE_GOLDEN") { + let oldpwd = std::env::var("BUILD_WORKSPACE_DIRECTORY") + .or_else(|_| std::env::var("OLDPWD")) + .expect("Neither BUILD_WORKSPACE_DIRECTORY nor OLDPWD env-var is set; unable to update golden"); + let Ok(manifest_dir) = std::env::var("CARGO_MANIFEST_DIR") else { + panic!("CARGO_MANIFEST_DIR env-var is not set; unable to update golden"); + }; + let crate_dir = Path::new(&oldpwd).join(manifest_dir); + let src_dir = match src_path.parent() { + Some(src_dir) => Path::new(&oldpwd).join(src_dir), + None => crate_dir, + }; + let full_path = src_dir.join(path); + + println!("Updating golden-data at path {full_path:?}"); + std::fs::write(full_path, actual).unwrap(); + return; + } + let update_command = if let Ok(test_target) = std::env::var("TEST_TARGET") { + format!("run ` UPDATE_GOLDEN=1 bazel run {test_target}`") + } else { + "run test with `UPDATE_GOLDEN=1` environment variable".into() + }; + panic!( + "Actual data differs from golden data;\n{}\nTo update golden data, {update_command}", + diff_lines.join("") + ) +} + +#[cfg(test)] +mod test { + + #[test] + fn test_success() { + assert_eq_golden!("foo\nbar\nbaz\n", "testdata/test1.golden") + } + + #[test] + #[should_panic(expected = "Actual data differs from golden data; +--- testdata/test1.golden\t ++++ testdata/test1.actual\t +@@ -1,3 +1,3 @@ + foo +-bar ++barr + baz + +To update golden data, run ` UPDATE_GOLDEN=1 bazel run ")] + fn test_failure() { + assert_eq_golden!("foo\nbarr\nbaz\n", "testdata/test1.golden") + } +} diff --git a/shared/golden/testdata/test1.golden b/shared/golden/testdata/test1.golden new file mode 100644 index 00000000..86e041da --- /dev/null +++ b/shared/golden/testdata/test1.golden @@ -0,0 +1,3 @@ +foo +bar +baz diff --git a/shared/openprot/proto/BUILD.bazel b/shared/openprot/proto/BUILD.bazel new file mode 100644 index 00000000..d62e32e9 --- /dev/null +++ b/shared/openprot/proto/BUILD.bazel @@ -0,0 +1,148 @@ +# Copyright 2026 Google LLC +# +# 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. + +load("@protobuf//bazel:proto_library.bzl", "proto_library") +load("@protobuf//rust:defs.bzl", "rust_proto_library") +load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") +load("//shared/tlv:rules.bzl", "tlv_codegen") + +package(default_visibility = ["//visibility:public"]) + +proto_library( + name = "chipid_proto", + srcs = ["chipid.proto"], + visibility = ["//visibility:public"], + deps = [ + "//shared/tlv:options_proto", + ], +) + +rust_proto_library( + name = "chipid_rust_proto", + tags = ["manual"], + visibility = ["//visibility:public"], + deps = [":chipid_proto"], +) + +tlv_codegen( + name = "chipid_tlv", + src = "chipid_tlv.rs", + proto = ":chipid_proto", + visibility = ["//visibility:public"], +) + +proto_library( + name = "crypto_proto", + srcs = ["crypto.proto"], + visibility = ["//visibility:public"], + deps = [ + "//shared/tlv:options_proto", + ], +) + +rust_proto_library( + name = "crypto_rust_proto", + tags = ["manual"], + visibility = ["//visibility:public"], + deps = [":crypto_proto"], +) + +tlv_codegen( + name = "crypto_tlv", + src = "crypto_tlv.rs", + proto = ":crypto_proto", + visibility = ["//visibility:public"], +) + +proto_library( + name = "identity_proto", + srcs = ["identity.proto"], + visibility = ["//visibility:public"], + deps = [ + "//shared/tlv:options_proto", + ], +) + +rust_proto_library( + name = "identity_rust_proto", + tags = ["manual"], + visibility = ["//visibility:public"], + deps = [":identity_proto"], +) + +tlv_codegen( + name = "identity_tlv", + src = "identity_tlv.rs", + proto = ":identity_proto", + visibility = ["//visibility:public"], +) + +proto_library( + name = "key_rotation_proto", + srcs = ["key_rotation.proto"], + visibility = ["//visibility:public"], + deps = [ + ":crypto_proto", + ":identity_proto", + "//shared/tlv:options_proto", + ], +) + +rust_proto_library( + name = "key_rotation_rust_proto", + tags = ["manual"], + visibility = ["//visibility:public"], + deps = [":key_rotation_proto"], +) + +tlv_codegen( + name = "key_rotation_tlv", + src = "key_rotation_tlv.rs", + proto = ":key_rotation_proto", + visibility = ["//visibility:public"], + deps = [ + ":crypto_tlv", + ":identity_tlv", + ], +) + +rust_library( + name = "proto", + srcs = [ + "lib.rs", + ], + edition = "2024", + visibility = ["//visibility:public"], + deps = [ + ":chipid_tlv", + ":crypto_tlv", + ":identity_tlv", + ":key_rotation_tlv", + "//shared/tlv", + "//third_party:zerocopy", + ], +) + +rust_test( + name = "proto_test", + crate = ":proto", + deps = [ + ":chipid_rust_proto", + ":crypto_rust_proto", + ":identity_rust_proto", + ":key_rotation_rust_proto", + "//shared/tlv:proto_conv", + "@protobuf//rust:protobuf", + ], +) diff --git a/shared/openprot/proto/chipid.proto b/shared/openprot/proto/chipid.proto new file mode 100644 index 00000000..ccef18b6 --- /dev/null +++ b/shared/openprot/proto/chipid.proto @@ -0,0 +1,34 @@ +// Copyright 2026 Google LLC +// +// 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. + +syntax = "proto3"; + +package openprot.chipid; + +import "shared/tlv/options.proto"; + +enum ChipFamily { + option (openprot.tlv.raw_type) = "u32"; + CHIP_FAMILY_UNKNOWN = 0; + CHIP_FAMILY_HOTH = 1; + CHIP_FAMILY_OPENTITAN = 2; +} + +message ChipId { + option (openprot.tlv.tlv_fourcc) = "C_ID"; + + ChipFamily chip_family = 1 [(openprot.tlv.offset) = 0]; + bytes chip_id = 2 + [(openprot.tlv.extra_data) = true, (openprot.tlv.type) = "[u8]"]; +} diff --git a/shared/openprot/proto/chipid_tlv.rs b/shared/openprot/proto/chipid_tlv.rs new file mode 100644 index 00000000..c00e0690 --- /dev/null +++ b/shared/openprot/proto/chipid_tlv.rs @@ -0,0 +1,82 @@ +// Copyright 2026 Google LLC +// +// 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. + +// Automatically generated by tlv_codegen. DO NOT EDIT. +// To regenerate, run: +// bazel run //shared/openprot/proto:chipid_tlv_update + +#![no_std] +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] +#![allow(unused_imports)] +#![allow(dead_code)] + +use bitfield_struct::bitfield; +use openprot_tlv::TlvFlags; +use openprot_tlv::TlvHeader; +use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout}; + +#[derive( + Clone, Copy, PartialEq, Eq, Hash, Default, FromBytes, IntoBytes, Immutable, KnownLayout, +)] +#[repr(transparent)] +pub struct ChipFamily(pub u32); +impl ChipFamily { + pub const UNKNOWN: Self = Self(0); + pub const HOTH: Self = Self(1); + pub const OPENTITAN: Self = Self(2); +} +impl ::core::fmt::Debug for ChipFamily { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + match *self { + Self::UNKNOWN => write!(f, "UNKNOWN"), + Self::HOTH => write!(f, "HOTH"), + Self::OPENTITAN => write!(f, "OPENTITAN"), + other => write!(f, "{}({})", stringify!(ChipFamily), other.0), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct ChipId { + pub chip_family: ChipFamily, +} +const _: () = { + assert!(::core::mem::offset_of!(ChipId, chip_family) == 0usize); + assert!(::core::mem::align_of::() <= 4); + assert!(::core::mem::size_of::() == 4usize); +}; +impl ::core::default::Default for ChipId { + fn default() -> Self { + ::zerocopy::FromZeros::new_zeroed() + } +} +impl ::openprot_tlv::TlvObject for ChipId { + type Extension = [u8]; + const TAG: u32 = u32::from_le_bytes(*b"C_ID"); + fn make_ext<'a>(header: &::openprot_tlv::TlvHeader, extra: &'a [u32]) -> &'a Self::Extension { + ::openprot_tlv::unpad(header, extra) + } +} +pub trait ChipIdExt<'a> { + fn chip_id(&self) -> &'a [u8]; +} +impl<'a> ChipIdExt<'a> for ::openprot_tlv::TlvItem<'a, ChipId> { + #[inline(always)] + fn chip_id(&self) -> &'a [u8] { + use ::openprot_tlv::TlvQuery; + self.ext() + } +} diff --git a/shared/openprot/proto/crypto.proto b/shared/openprot/proto/crypto.proto new file mode 100644 index 00000000..9a3d18e1 --- /dev/null +++ b/shared/openprot/proto/crypto.proto @@ -0,0 +1,335 @@ +// Copyright 2026 Google LLC +// +// 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. + +syntax = "proto3"; + +package openprot.crypto; + +import "shared/tlv/options.proto"; + +// The authority or purpose domain of a cryptographic key. +enum KeyDomain { + option (openprot.tlv.raw_type) = "u32"; + KEY_DOMAIN_UNSPECIFIED = 0; + KEY_DOMAIN_PAYLOAD_PROD = 0x444f5250; // "PROD" + KEY_DOMAIN_PAYLOAD_DEV = 0x5f564544; // "DEV_" + KEY_DOMAIN_PAYLOAD_TEST = 0x54534554; // "TEST" + KEY_DOMAIN_BIOS_PROD = 0x504f4942; // "BIOP" + KEY_DOMAIN_BIOS_DEV = 0x444f4942; // "BIOD" + KEY_DOMAIN_ROTR = 0x52544f52; // "ROTR" +} + +// ----------------------------------------------------------------------------- +// Cryptographic Hash Digests +// ----------------------------------------------------------------------------- + +message Sha2_256Digest { + option (openprot.tlv.tlv_fourcc) = "SH22"; + bytes data = 1 [(openprot.tlv.offset) = 0, (openprot.tlv.type) = "[u8; 32]"]; +} + +message Sha2_384Digest { + option (openprot.tlv.tlv_fourcc) = "SH23"; + bytes data = 1 [(openprot.tlv.offset) = 0, (openprot.tlv.type) = "[u8; 48]"]; +} + +message Sha2_512Digest { + option (openprot.tlv.tlv_fourcc) = "SH25"; + bytes data = 1 [(openprot.tlv.offset) = 0, (openprot.tlv.type) = "[u8; 64]"]; +} + +message Sha3_256Digest { + option (openprot.tlv.tlv_fourcc) = "SH32"; + bytes data = 1 [(openprot.tlv.offset) = 0, (openprot.tlv.type) = "[u8; 32]"]; +} + +message Sha3_384Digest { + option (openprot.tlv.tlv_fourcc) = "SH33"; + bytes data = 1 [(openprot.tlv.offset) = 0, (openprot.tlv.type) = "[u8; 48]"]; +} + +message Sha3_512Digest { + option (openprot.tlv.tlv_fourcc) = "SH35"; + bytes data = 1 [(openprot.tlv.offset) = 0, (openprot.tlv.type) = "[u8; 64]"]; +} + +// ----------------------------------------------------------------------------- +// Public Key Material & Public Keys +// ----------------------------------------------------------------------------- + +message P256PublicMaterial { + option (openprot.tlv.tlv_fourcc) = "P256"; + // P256 `x` coordinate in little-endian byte order. + bytes x = 1 [(openprot.tlv.offset) = 0, (openprot.tlv.type) = "[u32; 8]"]; + // P256 `y` coordinate in little-endian byte order. + bytes y = 2 [(openprot.tlv.offset) = 32, (openprot.tlv.type) = "[u32; 8]"]; +} + +message SlhDsaPublicMaterial { + option (openprot.tlv.tlv_fourcc) = "SPX+"; + // SLH-DSA SHA2-128s key material. + bytes slhdsa = 1 + [(openprot.tlv.offset) = 0, (openprot.tlv.type) = "[u32; 8]"]; +} + +message MlDsa44PublicMaterial { + option (openprot.tlv.tlv_fourcc) = "ML44"; + // ML-DSA-44 key material. + bytes mldsa = 1 + [(openprot.tlv.offset) = 0, (openprot.tlv.type) = "[u8; 1312]"]; +} + +message HybridP256SlhDsaPublicMaterial { + option (openprot.tlv.tlv_fourcc) = "H+S2"; + // P256 `x` coordinate in little-endian byte order. + bytes x = 1 [(openprot.tlv.offset) = 0, (openprot.tlv.type) = "[u32; 8]"]; + // P256 `y` coordinate in little-endian byte order. + bytes y = 2 [(openprot.tlv.offset) = 32, (openprot.tlv.type) = "[u32; 8]"]; + // SLH-DSA SHA2-128s key material. + bytes slhdsa = 3 + [(openprot.tlv.offset) = 64, (openprot.tlv.type) = "[u32; 8]"]; +} + +message HybridP256MlDsa44PublicMaterial { + option (openprot.tlv.tlv_fourcc) = "M4P2"; + // P256 `x` coordinate in little-endian byte order. + bytes x = 1 [(openprot.tlv.offset) = 0, (openprot.tlv.type) = "[u32; 8]"]; + // P256 `y` coordinate in little-endian byte order. + bytes y = 2 [(openprot.tlv.offset) = 32, (openprot.tlv.type) = "[u32; 8]"]; + // ML-DSA-44 key material. + bytes mldsa = 3 + [(openprot.tlv.offset) = 64, (openprot.tlv.type) = "[u8; 1312]"]; +} + +// The public exponent is not carried in this message: it is fixed at 65537 +// (0x10001, F4) for all supported RSA keys, so only the modulus is needed to +// reconstruct the public key. +message Rsa2048PublicMaterial { + option (openprot.tlv.tlv_fourcc) = "RSA2"; + // RSA-2048 public key modulus. + bytes modulus = 1 + [(openprot.tlv.offset) = 0, (openprot.tlv.type) = "[u8; 256]"]; +} + +// The public exponent is fixed at 65537 (0x10001, F4); see +// `Rsa2048PublicMaterial`. +message Rsa3072PublicMaterial { + option (openprot.tlv.tlv_fourcc) = "RSA3"; + // RSA-3072 public key modulus. + bytes modulus = 1 + [(openprot.tlv.offset) = 0, (openprot.tlv.type) = "[u8; 384]"]; +} + +// The public exponent is fixed at 65537 (0x10001, F4); see +// `Rsa2048PublicMaterial`. +message Rsa4096PublicMaterial { + option (openprot.tlv.tlv_fourcc) = "RSA4"; + // RSA-4096 public key modulus. + bytes modulus = 1 + [(openprot.tlv.offset) = 0, (openprot.tlv.type) = "[u8; 512]"]; +} + +message PublicKey { + option (openprot.tlv.tlv_fourcc) = "PKEY"; + + // Key domain: PROD, DEV, TEST, etc. + KeyDomain key_domain = 1 [(openprot.tlv.offset) = 0]; + + // Key material. + oneof key_material { + P256PublicMaterial p256 = 2; + SlhDsaPublicMaterial slhdsa = 3; + MlDsa44PublicMaterial mldsa = 4; + HybridP256SlhDsaPublicMaterial hybrid_p256_slhdsa = 5; + HybridP256MlDsa44PublicMaterial hybrid_p256_mldsa = 6; + Rsa2048PublicMaterial rsa2048 = 7; + Rsa3072PublicMaterial rsa3072 = 8; + Rsa4096PublicMaterial rsa4096 = 9; + } +} + +// ----------------------------------------------------------------------------- +// Signatures & Signature Containers +// ----------------------------------------------------------------------------- + +message P256Signature { + option (openprot.tlv.tlv_fourcc) = "P256"; + // P256 `r` in little-endian byte order. + bytes r = 1 [(openprot.tlv.offset) = 0, (openprot.tlv.type) = "[u32; 8]"]; + // P256 `s` in little-endian byte order. + bytes s = 2 [(openprot.tlv.offset) = 32, (openprot.tlv.type) = "[u32; 8]"]; +} + +message SlhDsaSignature { + option (openprot.tlv.tlv_fourcc) = "SPX+"; + // SLH-DSA signature. + bytes slhdsa = 1 + [(openprot.tlv.offset) = 0, (openprot.tlv.type) = "[u32; 1964]"]; +} + +message MlDsa44Signature { + option (openprot.tlv.tlv_fourcc) = "ML44"; + // ML-DSA-44 key signature. + bytes mldsa = 1 + [(openprot.tlv.offset) = 0, (openprot.tlv.type) = "[u8; 2420]"]; +} + +message HybridP256SlhDsaSignature { + option (openprot.tlv.tlv_fourcc) = "H+S2"; + // P256 `r` in little-endian byte order. + bytes r = 1 [(openprot.tlv.offset) = 0, (openprot.tlv.type) = "[u32; 8]"]; + // P256 `s` in little-endian byte order. + bytes s = 2 [(openprot.tlv.offset) = 32, (openprot.tlv.type) = "[u32; 8]"]; + // SLH-DSA signature. + bytes slhdsa = 3 + [(openprot.tlv.offset) = 64, (openprot.tlv.type) = "[u32; 1964]"]; +} + +message HybridP256MlDsa44Signature { + option (openprot.tlv.tlv_fourcc) = "M4P2"; + // P256 `r` in little-endian byte order. + bytes r = 1 [(openprot.tlv.offset) = 0, (openprot.tlv.type) = "[u32; 8]"]; + // P256 `s` in little-endian byte order. + bytes s = 2 [(openprot.tlv.offset) = 32, (openprot.tlv.type) = "[u32; 8]"]; + // ML-DSA-44 signature. + bytes mldsa = 3 + [(openprot.tlv.offset) = 64, (openprot.tlv.type) = "[u8; 2420]"]; +} + +message SignatureContainer { + option (openprot.tlv.tlv_fourcc) = "SIGC"; + + // The public key that will verify the signature in this container. + PublicKey public_key = 1; + // The signature over the descriptor TLV. + Signature signature = 2; +} + +message Signature { + option (openprot.tlv.tlv_fourcc) = "SIGN"; + + oneof signature { + P256Signature p256 = 1; + SlhDsaSignature slhdsa = 2; + MlDsa44Signature mldsa = 3; + HybridP256SlhDsaSignature hybrid_p256_slhdsa = 4; + HybridP256MlDsa44Signature hybrid_p256_mldsa = 5; + } +} + +// ----------------------------------------------------------------------------- +// Public Key Fingerprints +// ----------------------------------------------------------------------------- + +message P256Fingerprint { + option (openprot.tlv.tlv_fourcc) = "P256"; + oneof digest { + Sha2_256Digest sha2_256 = 1; + Sha2_384Digest sha2_384 = 2; + } +} + +message SlhDsaFingerprint { + option (openprot.tlv.tlv_fourcc) = "SPX+"; + oneof digest { + Sha2_256Digest sha2_256 = 1; + Sha2_384Digest sha2_384 = 2; + } +} + +message Rsa2048Fingerprint { + option (openprot.tlv.tlv_fourcc) = "RSA2"; + oneof digest { + Sha2_256Digest sha2_256 = 1; + Sha2_384Digest sha2_384 = 2; + } +} + +message Rsa3072Fingerprint { + option (openprot.tlv.tlv_fourcc) = "RSA3"; + oneof digest { + Sha2_256Digest sha2_256 = 1; + Sha2_384Digest sha2_384 = 2; + } +} + +message Rsa4096Fingerprint { + option (openprot.tlv.tlv_fourcc) = "RSA4"; + oneof digest { + Sha2_256Digest sha2_256 = 1; + Sha2_384Digest sha2_384 = 2; + } +} + +message HybridP256SlhDsaFingerprint { + option (openprot.tlv.tlv_fourcc) = "H+S2"; + oneof digest { + Sha2_256Digest sha2_256 = 1; + Sha2_384Digest sha2_384 = 2; + } +} + +message MlDsa44Fingerprint { + option (openprot.tlv.tlv_fourcc) = "ML44"; + oneof digest { + Sha2_256Digest sha2_256 = 1; + Sha2_384Digest sha2_384 = 2; + } +} + +message HybridP256MlDsa44Fingerprint { + option (openprot.tlv.tlv_fourcc) = "M4P2"; + oneof digest { + Sha2_256Digest sha2_256 = 1; + Sha2_384Digest sha2_384 = 2; + } +} + +// Public key fingerprint. +message KeyFingerprint { + option (openprot.tlv.tlv_fourcc) = "KFIP"; + + KeyDomain key_domain = 1 [(openprot.tlv.offset) = 0]; + + // Digest. + oneof key_type { + P256Fingerprint p256 = 2; + SlhDsaFingerprint slhdsa = 3; + Rsa2048Fingerprint rsa2048 = 4; + Rsa3072Fingerprint rsa3072 = 5; + Rsa4096Fingerprint rsa4096 = 6; + HybridP256SlhDsaFingerprint hybrid_p256_slhdsa = 7; + MlDsa44Fingerprint mldsa = 8; + HybridP256MlDsa44Fingerprint hybrid_p256_mldsa = 9; + } +} + +// ----------------------------------------------------------------------------- +// Symmetric Authentication & Hardware Sealing +// ----------------------------------------------------------------------------- + +// KMAC256 sealing ladder symmetric MAC. +message Kmac256Seal { + option (openprot.tlv.tlv_fourcc) = "KMSL"; + bytes data = 1 [(openprot.tlv.offset) = 0, (openprot.tlv.type) = "[u32; 8]"]; +} + +// Symmetric MAC seal generated by the firmware and stored in internal flash. +message KRSeal { + option (openprot.tlv.tlv_fourcc) = "SEAL"; + + oneof seal { + Kmac256Seal kmsl = 1; + } +} diff --git a/shared/openprot/proto/crypto_tlv.rs b/shared/openprot/proto/crypto_tlv.rs new file mode 100644 index 00000000..45651843 --- /dev/null +++ b/shared/openprot/proto/crypto_tlv.rs @@ -0,0 +1,1427 @@ +// Copyright 2026 Google LLC +// +// 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. + +// Automatically generated by tlv_codegen. DO NOT EDIT. +// To regenerate, run: +// bazel run //shared/openprot/proto:crypto_tlv_update + +#![no_std] +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] +#![allow(unused_imports)] +#![allow(dead_code)] + +use bitfield_struct::bitfield; +use openprot_tlv::TlvFlags; +use openprot_tlv::TlvHeader; +use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout}; + +#[derive( + Clone, Copy, PartialEq, Eq, Hash, Default, FromBytes, IntoBytes, Immutable, KnownLayout, +)] +#[repr(transparent)] +pub struct KeyDomain(pub u32); +impl KeyDomain { + pub const UNSPECIFIED: Self = Self(0); + pub const PAYLOAD_PROD: Self = Self(1146049104); + pub const PAYLOAD_DEV: Self = Self(1599489348); + pub const PAYLOAD_TEST: Self = Self(1414743380); + pub const BIOS_PROD: Self = Self(1347373378); + pub const BIOS_DEV: Self = Self(1146046786); + pub const ROTR: Self = Self(1381257042); +} +impl ::core::fmt::Debug for KeyDomain { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + match *self { + Self::UNSPECIFIED => write!(f, "UNSPECIFIED"), + Self::PAYLOAD_PROD => write!(f, "PAYLOAD_PROD"), + Self::PAYLOAD_DEV => write!(f, "PAYLOAD_DEV"), + Self::PAYLOAD_TEST => write!(f, "PAYLOAD_TEST"), + Self::BIOS_PROD => write!(f, "BIOS_PROD"), + Self::BIOS_DEV => write!(f, "BIOS_DEV"), + Self::ROTR => write!(f, "ROTR"), + other => write!(f, "{}({})", stringify!(KeyDomain), other.0), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct Sha2_256Digest { + pub data: [u8; 32], +} +const _: () = { + assert!(::core::mem::offset_of!(Sha2_256Digest, data) == 0usize); + assert!(::core::mem::align_of::() <= 4); + assert!(::core::mem::size_of::() == 32usize); +}; +impl ::core::default::Default for Sha2_256Digest { + fn default() -> Self { + ::zerocopy::FromZeros::new_zeroed() + } +} +impl ::openprot_tlv::TlvObject for Sha2_256Digest { + type Extension = [u8]; + const TAG: u32 = u32::from_le_bytes(*b"SH22"); + fn make_ext<'a>(header: &::openprot_tlv::TlvHeader, extra: &'a [u32]) -> &'a Self::Extension { + ::openprot_tlv::unpad(header, extra) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct Sha2_384Digest { + pub data: [u8; 48], +} +const _: () = { + assert!(::core::mem::offset_of!(Sha2_384Digest, data) == 0usize); + assert!(::core::mem::align_of::() <= 4); + assert!(::core::mem::size_of::() == 48usize); +}; +impl ::core::default::Default for Sha2_384Digest { + fn default() -> Self { + ::zerocopy::FromZeros::new_zeroed() + } +} +impl ::openprot_tlv::TlvObject for Sha2_384Digest { + type Extension = [u8]; + const TAG: u32 = u32::from_le_bytes(*b"SH23"); + fn make_ext<'a>(header: &::openprot_tlv::TlvHeader, extra: &'a [u32]) -> &'a Self::Extension { + ::openprot_tlv::unpad(header, extra) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct Sha2_512Digest { + pub data: [u8; 64], +} +const _: () = { + assert!(::core::mem::offset_of!(Sha2_512Digest, data) == 0usize); + assert!(::core::mem::align_of::() <= 4); + assert!(::core::mem::size_of::() == 64usize); +}; +impl ::core::default::Default for Sha2_512Digest { + fn default() -> Self { + ::zerocopy::FromZeros::new_zeroed() + } +} +impl ::openprot_tlv::TlvObject for Sha2_512Digest { + type Extension = [u8]; + const TAG: u32 = u32::from_le_bytes(*b"SH25"); + fn make_ext<'a>(header: &::openprot_tlv::TlvHeader, extra: &'a [u32]) -> &'a Self::Extension { + ::openprot_tlv::unpad(header, extra) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct Sha3_256Digest { + pub data: [u8; 32], +} +const _: () = { + assert!(::core::mem::offset_of!(Sha3_256Digest, data) == 0usize); + assert!(::core::mem::align_of::() <= 4); + assert!(::core::mem::size_of::() == 32usize); +}; +impl ::core::default::Default for Sha3_256Digest { + fn default() -> Self { + ::zerocopy::FromZeros::new_zeroed() + } +} +impl ::openprot_tlv::TlvObject for Sha3_256Digest { + type Extension = [u8]; + const TAG: u32 = u32::from_le_bytes(*b"SH32"); + fn make_ext<'a>(header: &::openprot_tlv::TlvHeader, extra: &'a [u32]) -> &'a Self::Extension { + ::openprot_tlv::unpad(header, extra) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct Sha3_384Digest { + pub data: [u8; 48], +} +const _: () = { + assert!(::core::mem::offset_of!(Sha3_384Digest, data) == 0usize); + assert!(::core::mem::align_of::() <= 4); + assert!(::core::mem::size_of::() == 48usize); +}; +impl ::core::default::Default for Sha3_384Digest { + fn default() -> Self { + ::zerocopy::FromZeros::new_zeroed() + } +} +impl ::openprot_tlv::TlvObject for Sha3_384Digest { + type Extension = [u8]; + const TAG: u32 = u32::from_le_bytes(*b"SH33"); + fn make_ext<'a>(header: &::openprot_tlv::TlvHeader, extra: &'a [u32]) -> &'a Self::Extension { + ::openprot_tlv::unpad(header, extra) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct Sha3_512Digest { + pub data: [u8; 64], +} +const _: () = { + assert!(::core::mem::offset_of!(Sha3_512Digest, data) == 0usize); + assert!(::core::mem::align_of::() <= 4); + assert!(::core::mem::size_of::() == 64usize); +}; +impl ::core::default::Default for Sha3_512Digest { + fn default() -> Self { + ::zerocopy::FromZeros::new_zeroed() + } +} +impl ::openprot_tlv::TlvObject for Sha3_512Digest { + type Extension = [u8]; + const TAG: u32 = u32::from_le_bytes(*b"SH35"); + fn make_ext<'a>(header: &::openprot_tlv::TlvHeader, extra: &'a [u32]) -> &'a Self::Extension { + ::openprot_tlv::unpad(header, extra) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct P256PublicMaterial { + pub x: [u32; 8], + pub y: [u32; 8], +} +const _: () = { + assert!(::core::mem::offset_of!(P256PublicMaterial, x) == 0usize); + assert!(::core::mem::offset_of!(P256PublicMaterial, y) == 32usize); + assert!(::core::mem::align_of::() <= 4); + assert!(::core::mem::size_of::() == 64usize); +}; +impl ::core::default::Default for P256PublicMaterial { + fn default() -> Self { + ::zerocopy::FromZeros::new_zeroed() + } +} +impl ::openprot_tlv::TlvObject for P256PublicMaterial { + type Extension = [u8]; + const TAG: u32 = u32::from_le_bytes(*b"P256"); + fn make_ext<'a>(header: &::openprot_tlv::TlvHeader, extra: &'a [u32]) -> &'a Self::Extension { + ::openprot_tlv::unpad(header, extra) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct SlhDsaPublicMaterial { + pub slhdsa: [u32; 8], +} +const _: () = { + assert!(::core::mem::offset_of!(SlhDsaPublicMaterial, slhdsa) == 0usize); + assert!(::core::mem::align_of::() <= 4); + assert!(::core::mem::size_of::() == 32usize); +}; +impl ::core::default::Default for SlhDsaPublicMaterial { + fn default() -> Self { + ::zerocopy::FromZeros::new_zeroed() + } +} +impl ::openprot_tlv::TlvObject for SlhDsaPublicMaterial { + type Extension = [u8]; + const TAG: u32 = u32::from_le_bytes(*b"SPX+"); + fn make_ext<'a>(header: &::openprot_tlv::TlvHeader, extra: &'a [u32]) -> &'a Self::Extension { + ::openprot_tlv::unpad(header, extra) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct MlDsa44PublicMaterial { + pub mldsa: [u8; 1312], +} +const _: () = { + assert!(::core::mem::offset_of!(MlDsa44PublicMaterial, mldsa) == 0usize); + assert!(::core::mem::align_of::() <= 4); + assert!(::core::mem::size_of::() == 1312usize); +}; +impl ::core::default::Default for MlDsa44PublicMaterial { + fn default() -> Self { + ::zerocopy::FromZeros::new_zeroed() + } +} +impl ::openprot_tlv::TlvObject for MlDsa44PublicMaterial { + type Extension = [u8]; + const TAG: u32 = u32::from_le_bytes(*b"ML44"); + fn make_ext<'a>(header: &::openprot_tlv::TlvHeader, extra: &'a [u32]) -> &'a Self::Extension { + ::openprot_tlv::unpad(header, extra) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct HybridP256SlhDsaPublicMaterial { + pub x: [u32; 8], + pub y: [u32; 8], + pub slhdsa: [u32; 8], +} +const _: () = { + assert!(::core::mem::offset_of!(HybridP256SlhDsaPublicMaterial, x) == 0usize); + assert!(::core::mem::offset_of!(HybridP256SlhDsaPublicMaterial, y) == 32usize); + assert!(::core::mem::offset_of!(HybridP256SlhDsaPublicMaterial, slhdsa) == 64usize); + assert!(::core::mem::align_of::() <= 4); + assert!(::core::mem::size_of::() == 96usize); +}; +impl ::core::default::Default for HybridP256SlhDsaPublicMaterial { + fn default() -> Self { + ::zerocopy::FromZeros::new_zeroed() + } +} +impl ::openprot_tlv::TlvObject for HybridP256SlhDsaPublicMaterial { + type Extension = [u8]; + const TAG: u32 = u32::from_le_bytes(*b"H+S2"); + fn make_ext<'a>(header: &::openprot_tlv::TlvHeader, extra: &'a [u32]) -> &'a Self::Extension { + ::openprot_tlv::unpad(header, extra) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct HybridP256MlDsa44PublicMaterial { + pub x: [u32; 8], + pub y: [u32; 8], + pub mldsa: [u8; 1312], +} +const _: () = { + assert!(::core::mem::offset_of!(HybridP256MlDsa44PublicMaterial, x) == 0usize); + assert!(::core::mem::offset_of!(HybridP256MlDsa44PublicMaterial, y) == 32usize); + assert!(::core::mem::offset_of!(HybridP256MlDsa44PublicMaterial, mldsa) == 64usize); + assert!(::core::mem::align_of::() <= 4); + assert!(::core::mem::size_of::() == 1376usize); +}; +impl ::core::default::Default for HybridP256MlDsa44PublicMaterial { + fn default() -> Self { + ::zerocopy::FromZeros::new_zeroed() + } +} +impl ::openprot_tlv::TlvObject for HybridP256MlDsa44PublicMaterial { + type Extension = [u8]; + const TAG: u32 = u32::from_le_bytes(*b"M4P2"); + fn make_ext<'a>(header: &::openprot_tlv::TlvHeader, extra: &'a [u32]) -> &'a Self::Extension { + ::openprot_tlv::unpad(header, extra) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct Rsa2048PublicMaterial { + pub modulus: [u8; 256], +} +const _: () = { + assert!(::core::mem::offset_of!(Rsa2048PublicMaterial, modulus) == 0usize); + assert!(::core::mem::align_of::() <= 4); + assert!(::core::mem::size_of::() == 256usize); +}; +impl ::core::default::Default for Rsa2048PublicMaterial { + fn default() -> Self { + ::zerocopy::FromZeros::new_zeroed() + } +} +impl ::openprot_tlv::TlvObject for Rsa2048PublicMaterial { + type Extension = [u8]; + const TAG: u32 = u32::from_le_bytes(*b"RSA2"); + fn make_ext<'a>(header: &::openprot_tlv::TlvHeader, extra: &'a [u32]) -> &'a Self::Extension { + ::openprot_tlv::unpad(header, extra) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct Rsa3072PublicMaterial { + pub modulus: [u8; 384], +} +const _: () = { + assert!(::core::mem::offset_of!(Rsa3072PublicMaterial, modulus) == 0usize); + assert!(::core::mem::align_of::() <= 4); + assert!(::core::mem::size_of::() == 384usize); +}; +impl ::core::default::Default for Rsa3072PublicMaterial { + fn default() -> Self { + ::zerocopy::FromZeros::new_zeroed() + } +} +impl ::openprot_tlv::TlvObject for Rsa3072PublicMaterial { + type Extension = [u8]; + const TAG: u32 = u32::from_le_bytes(*b"RSA3"); + fn make_ext<'a>(header: &::openprot_tlv::TlvHeader, extra: &'a [u32]) -> &'a Self::Extension { + ::openprot_tlv::unpad(header, extra) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct Rsa4096PublicMaterial { + pub modulus: [u8; 512], +} +const _: () = { + assert!(::core::mem::offset_of!(Rsa4096PublicMaterial, modulus) == 0usize); + assert!(::core::mem::align_of::() <= 4); + assert!(::core::mem::size_of::() == 512usize); +}; +impl ::core::default::Default for Rsa4096PublicMaterial { + fn default() -> Self { + ::zerocopy::FromZeros::new_zeroed() + } +} +impl ::openprot_tlv::TlvObject for Rsa4096PublicMaterial { + type Extension = [u8]; + const TAG: u32 = u32::from_le_bytes(*b"RSA4"); + fn make_ext<'a>(header: &::openprot_tlv::TlvHeader, extra: &'a [u32]) -> &'a Self::Extension { + ::openprot_tlv::unpad(header, extra) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct PublicKey { + pub key_domain: KeyDomain, +} +const _: () = { + assert!(::core::mem::offset_of!(PublicKey, key_domain) == 0usize); + assert!(::core::mem::align_of::() <= 4); + assert!(::core::mem::size_of::() == 4usize); +}; +impl ::core::default::Default for PublicKey { + fn default() -> Self { + ::zerocopy::FromZeros::new_zeroed() + } +} +impl ::openprot_tlv::TlvObject for PublicKey { + type Extension = ::openprot_tlv::TlvData; + const TAG: u32 = u32::from_le_bytes(*b"PKEY"); + fn make_ext<'a>(_header: &::openprot_tlv::TlvHeader, extra: &'a [u32]) -> &'a Self::Extension { + ::openprot_tlv::TlvData::overlay(extra) + } +} +pub enum PublicKeyKeyMaterial<'a> { + P256(::openprot_tlv::TlvItem<'a, P256PublicMaterial>), + Slhdsa(::openprot_tlv::TlvItem<'a, SlhDsaPublicMaterial>), + Mldsa(::openprot_tlv::TlvItem<'a, MlDsa44PublicMaterial>), + HybridP256Slhdsa(::openprot_tlv::TlvItem<'a, HybridP256SlhDsaPublicMaterial>), + HybridP256Mldsa(::openprot_tlv::TlvItem<'a, HybridP256MlDsa44PublicMaterial>), + Rsa2048(::openprot_tlv::TlvItem<'a, Rsa2048PublicMaterial>), + Rsa3072(::openprot_tlv::TlvItem<'a, Rsa3072PublicMaterial>), + Rsa4096(::openprot_tlv::TlvItem<'a, Rsa4096PublicMaterial>), +} +pub trait PublicKeyExt<'a> { + fn p256(&self) -> Option<::openprot_tlv::TlvItem<'a, P256PublicMaterial>>; + fn slhdsa(&self) -> Option<::openprot_tlv::TlvItem<'a, SlhDsaPublicMaterial>>; + fn mldsa(&self) -> Option<::openprot_tlv::TlvItem<'a, MlDsa44PublicMaterial>>; + fn hybrid_p256_slhdsa( + &self, + ) -> Option<::openprot_tlv::TlvItem<'a, HybridP256SlhDsaPublicMaterial>>; + fn hybrid_p256_mldsa( + &self, + ) -> Option<::openprot_tlv::TlvItem<'a, HybridP256MlDsa44PublicMaterial>>; + fn rsa2048(&self) -> Option<::openprot_tlv::TlvItem<'a, Rsa2048PublicMaterial>>; + fn rsa3072(&self) -> Option<::openprot_tlv::TlvItem<'a, Rsa3072PublicMaterial>>; + fn rsa4096(&self) -> Option<::openprot_tlv::TlvItem<'a, Rsa4096PublicMaterial>>; + fn key_material(&self) -> Option>; +} +impl<'a> PublicKeyExt<'a> for ::openprot_tlv::TlvItem<'a, PublicKey> { + #[inline(always)] + fn p256(&self) -> Option<::openprot_tlv::TlvItem<'a, P256PublicMaterial>> { + ::openprot_tlv::TlvQuery::ext(self) + .iter::() + .next() + } + #[inline(always)] + fn slhdsa(&self) -> Option<::openprot_tlv::TlvItem<'a, SlhDsaPublicMaterial>> { + ::openprot_tlv::TlvQuery::ext(self) + .iter::() + .next() + } + #[inline(always)] + fn mldsa(&self) -> Option<::openprot_tlv::TlvItem<'a, MlDsa44PublicMaterial>> { + ::openprot_tlv::TlvQuery::ext(self) + .iter::() + .next() + } + #[inline(always)] + fn hybrid_p256_slhdsa( + &self, + ) -> Option<::openprot_tlv::TlvItem<'a, HybridP256SlhDsaPublicMaterial>> { + ::openprot_tlv::TlvQuery::ext(self) + .iter::() + .next() + } + #[inline(always)] + fn hybrid_p256_mldsa( + &self, + ) -> Option<::openprot_tlv::TlvItem<'a, HybridP256MlDsa44PublicMaterial>> { + ::openprot_tlv::TlvQuery::ext(self) + .iter::() + .next() + } + #[inline(always)] + fn rsa2048(&self) -> Option<::openprot_tlv::TlvItem<'a, Rsa2048PublicMaterial>> { + ::openprot_tlv::TlvQuery::ext(self) + .iter::() + .next() + } + #[inline(always)] + fn rsa3072(&self) -> Option<::openprot_tlv::TlvItem<'a, Rsa3072PublicMaterial>> { + ::openprot_tlv::TlvQuery::ext(self) + .iter::() + .next() + } + #[inline(always)] + fn rsa4096(&self) -> Option<::openprot_tlv::TlvItem<'a, Rsa4096PublicMaterial>> { + ::openprot_tlv::TlvQuery::ext(self) + .iter::() + .next() + } + #[inline(always)] + fn key_material(&self) -> Option> { + use ::openprot_tlv::TlvQuery; + for item in ::openprot_tlv::TlvQuery::ext(self).iter::<::openprot_tlv::TlvAny>() { + if let Some(val) = item.cast::() { + return Some(PublicKeyKeyMaterial::P256(val)); + } + if let Some(val) = item.cast::() { + return Some(PublicKeyKeyMaterial::Slhdsa(val)); + } + if let Some(val) = item.cast::() { + return Some(PublicKeyKeyMaterial::Mldsa(val)); + } + if let Some(val) = item.cast::() { + return Some(PublicKeyKeyMaterial::HybridP256Slhdsa(val)); + } + if let Some(val) = item.cast::() { + return Some(PublicKeyKeyMaterial::HybridP256Mldsa(val)); + } + if let Some(val) = item.cast::() { + return Some(PublicKeyKeyMaterial::Rsa2048(val)); + } + if let Some(val) = item.cast::() { + return Some(PublicKeyKeyMaterial::Rsa3072(val)); + } + if let Some(val) = item.cast::() { + return Some(PublicKeyKeyMaterial::Rsa4096(val)); + } + } + None + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct P256Signature { + pub r: [u32; 8], + pub s: [u32; 8], +} +const _: () = { + assert!(::core::mem::offset_of!(P256Signature, r) == 0usize); + assert!(::core::mem::offset_of!(P256Signature, s) == 32usize); + assert!(::core::mem::align_of::() <= 4); + assert!(::core::mem::size_of::() == 64usize); +}; +impl ::core::default::Default for P256Signature { + fn default() -> Self { + ::zerocopy::FromZeros::new_zeroed() + } +} +impl ::openprot_tlv::TlvObject for P256Signature { + type Extension = [u8]; + const TAG: u32 = u32::from_le_bytes(*b"P256"); + fn make_ext<'a>(header: &::openprot_tlv::TlvHeader, extra: &'a [u32]) -> &'a Self::Extension { + ::openprot_tlv::unpad(header, extra) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct SlhDsaSignature { + pub slhdsa: [u32; 1964], +} +const _: () = { + assert!(::core::mem::offset_of!(SlhDsaSignature, slhdsa) == 0usize); + assert!(::core::mem::align_of::() <= 4); + assert!(::core::mem::size_of::() == 7856usize); +}; +impl ::core::default::Default for SlhDsaSignature { + fn default() -> Self { + ::zerocopy::FromZeros::new_zeroed() + } +} +impl ::openprot_tlv::TlvObject for SlhDsaSignature { + type Extension = [u8]; + const TAG: u32 = u32::from_le_bytes(*b"SPX+"); + fn make_ext<'a>(header: &::openprot_tlv::TlvHeader, extra: &'a [u32]) -> &'a Self::Extension { + ::openprot_tlv::unpad(header, extra) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct MlDsa44Signature { + pub mldsa: [u8; 2420], +} +const _: () = { + assert!(::core::mem::offset_of!(MlDsa44Signature, mldsa) == 0usize); + assert!(::core::mem::align_of::() <= 4); + assert!(::core::mem::size_of::() == 2420usize); +}; +impl ::core::default::Default for MlDsa44Signature { + fn default() -> Self { + ::zerocopy::FromZeros::new_zeroed() + } +} +impl ::openprot_tlv::TlvObject for MlDsa44Signature { + type Extension = [u8]; + const TAG: u32 = u32::from_le_bytes(*b"ML44"); + fn make_ext<'a>(header: &::openprot_tlv::TlvHeader, extra: &'a [u32]) -> &'a Self::Extension { + ::openprot_tlv::unpad(header, extra) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct HybridP256SlhDsaSignature { + pub r: [u32; 8], + pub s: [u32; 8], + pub slhdsa: [u32; 1964], +} +const _: () = { + assert!(::core::mem::offset_of!(HybridP256SlhDsaSignature, r) == 0usize); + assert!(::core::mem::offset_of!(HybridP256SlhDsaSignature, s) == 32usize); + assert!(::core::mem::offset_of!(HybridP256SlhDsaSignature, slhdsa) == 64usize); + assert!(::core::mem::align_of::() <= 4); + assert!(::core::mem::size_of::() == 7920usize); +}; +impl ::core::default::Default for HybridP256SlhDsaSignature { + fn default() -> Self { + ::zerocopy::FromZeros::new_zeroed() + } +} +impl ::openprot_tlv::TlvObject for HybridP256SlhDsaSignature { + type Extension = [u8]; + const TAG: u32 = u32::from_le_bytes(*b"H+S2"); + fn make_ext<'a>(header: &::openprot_tlv::TlvHeader, extra: &'a [u32]) -> &'a Self::Extension { + ::openprot_tlv::unpad(header, extra) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct HybridP256MlDsa44Signature { + pub r: [u32; 8], + pub s: [u32; 8], + pub mldsa: [u8; 2420], +} +const _: () = { + assert!(::core::mem::offset_of!(HybridP256MlDsa44Signature, r) == 0usize); + assert!(::core::mem::offset_of!(HybridP256MlDsa44Signature, s) == 32usize); + assert!(::core::mem::offset_of!(HybridP256MlDsa44Signature, mldsa) == 64usize); + assert!(::core::mem::align_of::() <= 4); + assert!(::core::mem::size_of::() == 2484usize); +}; +impl ::core::default::Default for HybridP256MlDsa44Signature { + fn default() -> Self { + ::zerocopy::FromZeros::new_zeroed() + } +} +impl ::openprot_tlv::TlvObject for HybridP256MlDsa44Signature { + type Extension = [u8]; + const TAG: u32 = u32::from_le_bytes(*b"M4P2"); + fn make_ext<'a>(header: &::openprot_tlv::TlvHeader, extra: &'a [u32]) -> &'a Self::Extension { + ::openprot_tlv::unpad(header, extra) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct SignatureContainer {} +const _: () = { + assert!(::core::mem::align_of::() <= 4); + assert!(::core::mem::size_of::() == 0usize); +}; +impl ::core::default::Default for SignatureContainer { + fn default() -> Self { + ::zerocopy::FromZeros::new_zeroed() + } +} +impl ::openprot_tlv::TlvObject for SignatureContainer { + type Extension = ::openprot_tlv::TlvData; + const TAG: u32 = u32::from_le_bytes(*b"SIGC"); + fn make_ext<'a>(_header: &::openprot_tlv::TlvHeader, extra: &'a [u32]) -> &'a Self::Extension { + ::openprot_tlv::TlvData::overlay(extra) + } +} +pub trait SignatureContainerExt<'a> { + fn public_key(&self) -> Option<::openprot_tlv::TlvItem<'a, PublicKey>>; + fn signature(&self) -> Option<::openprot_tlv::TlvItem<'a, Signature>>; +} +impl<'a> SignatureContainerExt<'a> for ::openprot_tlv::TlvItem<'a, SignatureContainer> { + #[inline(always)] + fn public_key(&self) -> Option<::openprot_tlv::TlvItem<'a, PublicKey>> { + ::openprot_tlv::TlvQuery::ext(self) + .iter::() + .next() + } + #[inline(always)] + fn signature(&self) -> Option<::openprot_tlv::TlvItem<'a, Signature>> { + ::openprot_tlv::TlvQuery::ext(self) + .iter::() + .next() + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct Signature {} +const _: () = { + assert!(::core::mem::align_of::() <= 4); + assert!(::core::mem::size_of::() == 0usize); +}; +impl ::core::default::Default for Signature { + fn default() -> Self { + ::zerocopy::FromZeros::new_zeroed() + } +} +impl ::openprot_tlv::TlvObject for Signature { + type Extension = ::openprot_tlv::TlvData; + const TAG: u32 = u32::from_le_bytes(*b"SIGN"); + fn make_ext<'a>(_header: &::openprot_tlv::TlvHeader, extra: &'a [u32]) -> &'a Self::Extension { + ::openprot_tlv::TlvData::overlay(extra) + } +} +pub enum SignatureSignature<'a> { + P256(::openprot_tlv::TlvItem<'a, P256Signature>), + Slhdsa(::openprot_tlv::TlvItem<'a, SlhDsaSignature>), + Mldsa(::openprot_tlv::TlvItem<'a, MlDsa44Signature>), + HybridP256Slhdsa(::openprot_tlv::TlvItem<'a, HybridP256SlhDsaSignature>), + HybridP256Mldsa(::openprot_tlv::TlvItem<'a, HybridP256MlDsa44Signature>), +} +pub trait SignatureExt<'a> { + fn p256(&self) -> Option<::openprot_tlv::TlvItem<'a, P256Signature>>; + fn slhdsa(&self) -> Option<::openprot_tlv::TlvItem<'a, SlhDsaSignature>>; + fn mldsa(&self) -> Option<::openprot_tlv::TlvItem<'a, MlDsa44Signature>>; + fn hybrid_p256_slhdsa(&self) -> Option<::openprot_tlv::TlvItem<'a, HybridP256SlhDsaSignature>>; + fn hybrid_p256_mldsa(&self) -> Option<::openprot_tlv::TlvItem<'a, HybridP256MlDsa44Signature>>; + fn signature(&self) -> Option>; +} +impl<'a> SignatureExt<'a> for ::openprot_tlv::TlvItem<'a, Signature> { + #[inline(always)] + fn p256(&self) -> Option<::openprot_tlv::TlvItem<'a, P256Signature>> { + ::openprot_tlv::TlvQuery::ext(self) + .iter::() + .next() + } + #[inline(always)] + fn slhdsa(&self) -> Option<::openprot_tlv::TlvItem<'a, SlhDsaSignature>> { + ::openprot_tlv::TlvQuery::ext(self) + .iter::() + .next() + } + #[inline(always)] + fn mldsa(&self) -> Option<::openprot_tlv::TlvItem<'a, MlDsa44Signature>> { + ::openprot_tlv::TlvQuery::ext(self) + .iter::() + .next() + } + #[inline(always)] + fn hybrid_p256_slhdsa(&self) -> Option<::openprot_tlv::TlvItem<'a, HybridP256SlhDsaSignature>> { + ::openprot_tlv::TlvQuery::ext(self) + .iter::() + .next() + } + #[inline(always)] + fn hybrid_p256_mldsa(&self) -> Option<::openprot_tlv::TlvItem<'a, HybridP256MlDsa44Signature>> { + ::openprot_tlv::TlvQuery::ext(self) + .iter::() + .next() + } + #[inline(always)] + fn signature(&self) -> Option> { + use ::openprot_tlv::TlvQuery; + for item in ::openprot_tlv::TlvQuery::ext(self).iter::<::openprot_tlv::TlvAny>() { + if let Some(val) = item.cast::() { + return Some(SignatureSignature::P256(val)); + } + if let Some(val) = item.cast::() { + return Some(SignatureSignature::Slhdsa(val)); + } + if let Some(val) = item.cast::() { + return Some(SignatureSignature::Mldsa(val)); + } + if let Some(val) = item.cast::() { + return Some(SignatureSignature::HybridP256Slhdsa(val)); + } + if let Some(val) = item.cast::() { + return Some(SignatureSignature::HybridP256Mldsa(val)); + } + } + None + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct P256Fingerprint {} +const _: () = { + assert!(::core::mem::align_of::() <= 4); + assert!(::core::mem::size_of::() == 0usize); +}; +impl ::core::default::Default for P256Fingerprint { + fn default() -> Self { + ::zerocopy::FromZeros::new_zeroed() + } +} +impl ::openprot_tlv::TlvObject for P256Fingerprint { + type Extension = ::openprot_tlv::TlvData; + const TAG: u32 = u32::from_le_bytes(*b"P256"); + fn make_ext<'a>(_header: &::openprot_tlv::TlvHeader, extra: &'a [u32]) -> &'a Self::Extension { + ::openprot_tlv::TlvData::overlay(extra) + } +} +pub enum P256FingerprintDigest<'a> { + Sha2256(::openprot_tlv::TlvItem<'a, Sha2_256Digest>), + Sha2384(::openprot_tlv::TlvItem<'a, Sha2_384Digest>), +} +pub trait P256FingerprintExt<'a> { + fn sha2_256(&self) -> Option<::openprot_tlv::TlvItem<'a, Sha2_256Digest>>; + fn sha2_384(&self) -> Option<::openprot_tlv::TlvItem<'a, Sha2_384Digest>>; + fn digest(&self) -> Option>; +} +impl<'a> P256FingerprintExt<'a> for ::openprot_tlv::TlvItem<'a, P256Fingerprint> { + #[inline(always)] + fn sha2_256(&self) -> Option<::openprot_tlv::TlvItem<'a, Sha2_256Digest>> { + ::openprot_tlv::TlvQuery::ext(self) + .iter::() + .next() + } + #[inline(always)] + fn sha2_384(&self) -> Option<::openprot_tlv::TlvItem<'a, Sha2_384Digest>> { + ::openprot_tlv::TlvQuery::ext(self) + .iter::() + .next() + } + #[inline(always)] + fn digest(&self) -> Option> { + use ::openprot_tlv::TlvQuery; + for item in ::openprot_tlv::TlvQuery::ext(self).iter::<::openprot_tlv::TlvAny>() { + if let Some(val) = item.cast::() { + return Some(P256FingerprintDigest::Sha2256(val)); + } + if let Some(val) = item.cast::() { + return Some(P256FingerprintDigest::Sha2384(val)); + } + } + None + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct SlhDsaFingerprint {} +const _: () = { + assert!(::core::mem::align_of::() <= 4); + assert!(::core::mem::size_of::() == 0usize); +}; +impl ::core::default::Default for SlhDsaFingerprint { + fn default() -> Self { + ::zerocopy::FromZeros::new_zeroed() + } +} +impl ::openprot_tlv::TlvObject for SlhDsaFingerprint { + type Extension = ::openprot_tlv::TlvData; + const TAG: u32 = u32::from_le_bytes(*b"SPX+"); + fn make_ext<'a>(_header: &::openprot_tlv::TlvHeader, extra: &'a [u32]) -> &'a Self::Extension { + ::openprot_tlv::TlvData::overlay(extra) + } +} +pub enum SlhDsaFingerprintDigest<'a> { + Sha2256(::openprot_tlv::TlvItem<'a, Sha2_256Digest>), + Sha2384(::openprot_tlv::TlvItem<'a, Sha2_384Digest>), +} +pub trait SlhDsaFingerprintExt<'a> { + fn sha2_256(&self) -> Option<::openprot_tlv::TlvItem<'a, Sha2_256Digest>>; + fn sha2_384(&self) -> Option<::openprot_tlv::TlvItem<'a, Sha2_384Digest>>; + fn digest(&self) -> Option>; +} +impl<'a> SlhDsaFingerprintExt<'a> for ::openprot_tlv::TlvItem<'a, SlhDsaFingerprint> { + #[inline(always)] + fn sha2_256(&self) -> Option<::openprot_tlv::TlvItem<'a, Sha2_256Digest>> { + ::openprot_tlv::TlvQuery::ext(self) + .iter::() + .next() + } + #[inline(always)] + fn sha2_384(&self) -> Option<::openprot_tlv::TlvItem<'a, Sha2_384Digest>> { + ::openprot_tlv::TlvQuery::ext(self) + .iter::() + .next() + } + #[inline(always)] + fn digest(&self) -> Option> { + use ::openprot_tlv::TlvQuery; + for item in ::openprot_tlv::TlvQuery::ext(self).iter::<::openprot_tlv::TlvAny>() { + if let Some(val) = item.cast::() { + return Some(SlhDsaFingerprintDigest::Sha2256(val)); + } + if let Some(val) = item.cast::() { + return Some(SlhDsaFingerprintDigest::Sha2384(val)); + } + } + None + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct Rsa2048Fingerprint {} +const _: () = { + assert!(::core::mem::align_of::() <= 4); + assert!(::core::mem::size_of::() == 0usize); +}; +impl ::core::default::Default for Rsa2048Fingerprint { + fn default() -> Self { + ::zerocopy::FromZeros::new_zeroed() + } +} +impl ::openprot_tlv::TlvObject for Rsa2048Fingerprint { + type Extension = ::openprot_tlv::TlvData; + const TAG: u32 = u32::from_le_bytes(*b"RSA2"); + fn make_ext<'a>(_header: &::openprot_tlv::TlvHeader, extra: &'a [u32]) -> &'a Self::Extension { + ::openprot_tlv::TlvData::overlay(extra) + } +} +pub enum Rsa2048FingerprintDigest<'a> { + Sha2256(::openprot_tlv::TlvItem<'a, Sha2_256Digest>), + Sha2384(::openprot_tlv::TlvItem<'a, Sha2_384Digest>), +} +pub trait Rsa2048FingerprintExt<'a> { + fn sha2_256(&self) -> Option<::openprot_tlv::TlvItem<'a, Sha2_256Digest>>; + fn sha2_384(&self) -> Option<::openprot_tlv::TlvItem<'a, Sha2_384Digest>>; + fn digest(&self) -> Option>; +} +impl<'a> Rsa2048FingerprintExt<'a> for ::openprot_tlv::TlvItem<'a, Rsa2048Fingerprint> { + #[inline(always)] + fn sha2_256(&self) -> Option<::openprot_tlv::TlvItem<'a, Sha2_256Digest>> { + ::openprot_tlv::TlvQuery::ext(self) + .iter::() + .next() + } + #[inline(always)] + fn sha2_384(&self) -> Option<::openprot_tlv::TlvItem<'a, Sha2_384Digest>> { + ::openprot_tlv::TlvQuery::ext(self) + .iter::() + .next() + } + #[inline(always)] + fn digest(&self) -> Option> { + use ::openprot_tlv::TlvQuery; + for item in ::openprot_tlv::TlvQuery::ext(self).iter::<::openprot_tlv::TlvAny>() { + if let Some(val) = item.cast::() { + return Some(Rsa2048FingerprintDigest::Sha2256(val)); + } + if let Some(val) = item.cast::() { + return Some(Rsa2048FingerprintDigest::Sha2384(val)); + } + } + None + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct Rsa3072Fingerprint {} +const _: () = { + assert!(::core::mem::align_of::() <= 4); + assert!(::core::mem::size_of::() == 0usize); +}; +impl ::core::default::Default for Rsa3072Fingerprint { + fn default() -> Self { + ::zerocopy::FromZeros::new_zeroed() + } +} +impl ::openprot_tlv::TlvObject for Rsa3072Fingerprint { + type Extension = ::openprot_tlv::TlvData; + const TAG: u32 = u32::from_le_bytes(*b"RSA3"); + fn make_ext<'a>(_header: &::openprot_tlv::TlvHeader, extra: &'a [u32]) -> &'a Self::Extension { + ::openprot_tlv::TlvData::overlay(extra) + } +} +pub enum Rsa3072FingerprintDigest<'a> { + Sha2256(::openprot_tlv::TlvItem<'a, Sha2_256Digest>), + Sha2384(::openprot_tlv::TlvItem<'a, Sha2_384Digest>), +} +pub trait Rsa3072FingerprintExt<'a> { + fn sha2_256(&self) -> Option<::openprot_tlv::TlvItem<'a, Sha2_256Digest>>; + fn sha2_384(&self) -> Option<::openprot_tlv::TlvItem<'a, Sha2_384Digest>>; + fn digest(&self) -> Option>; +} +impl<'a> Rsa3072FingerprintExt<'a> for ::openprot_tlv::TlvItem<'a, Rsa3072Fingerprint> { + #[inline(always)] + fn sha2_256(&self) -> Option<::openprot_tlv::TlvItem<'a, Sha2_256Digest>> { + ::openprot_tlv::TlvQuery::ext(self) + .iter::() + .next() + } + #[inline(always)] + fn sha2_384(&self) -> Option<::openprot_tlv::TlvItem<'a, Sha2_384Digest>> { + ::openprot_tlv::TlvQuery::ext(self) + .iter::() + .next() + } + #[inline(always)] + fn digest(&self) -> Option> { + use ::openprot_tlv::TlvQuery; + for item in ::openprot_tlv::TlvQuery::ext(self).iter::<::openprot_tlv::TlvAny>() { + if let Some(val) = item.cast::() { + return Some(Rsa3072FingerprintDigest::Sha2256(val)); + } + if let Some(val) = item.cast::() { + return Some(Rsa3072FingerprintDigest::Sha2384(val)); + } + } + None + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct Rsa4096Fingerprint {} +const _: () = { + assert!(::core::mem::align_of::() <= 4); + assert!(::core::mem::size_of::() == 0usize); +}; +impl ::core::default::Default for Rsa4096Fingerprint { + fn default() -> Self { + ::zerocopy::FromZeros::new_zeroed() + } +} +impl ::openprot_tlv::TlvObject for Rsa4096Fingerprint { + type Extension = ::openprot_tlv::TlvData; + const TAG: u32 = u32::from_le_bytes(*b"RSA4"); + fn make_ext<'a>(_header: &::openprot_tlv::TlvHeader, extra: &'a [u32]) -> &'a Self::Extension { + ::openprot_tlv::TlvData::overlay(extra) + } +} +pub enum Rsa4096FingerprintDigest<'a> { + Sha2256(::openprot_tlv::TlvItem<'a, Sha2_256Digest>), + Sha2384(::openprot_tlv::TlvItem<'a, Sha2_384Digest>), +} +pub trait Rsa4096FingerprintExt<'a> { + fn sha2_256(&self) -> Option<::openprot_tlv::TlvItem<'a, Sha2_256Digest>>; + fn sha2_384(&self) -> Option<::openprot_tlv::TlvItem<'a, Sha2_384Digest>>; + fn digest(&self) -> Option>; +} +impl<'a> Rsa4096FingerprintExt<'a> for ::openprot_tlv::TlvItem<'a, Rsa4096Fingerprint> { + #[inline(always)] + fn sha2_256(&self) -> Option<::openprot_tlv::TlvItem<'a, Sha2_256Digest>> { + ::openprot_tlv::TlvQuery::ext(self) + .iter::() + .next() + } + #[inline(always)] + fn sha2_384(&self) -> Option<::openprot_tlv::TlvItem<'a, Sha2_384Digest>> { + ::openprot_tlv::TlvQuery::ext(self) + .iter::() + .next() + } + #[inline(always)] + fn digest(&self) -> Option> { + use ::openprot_tlv::TlvQuery; + for item in ::openprot_tlv::TlvQuery::ext(self).iter::<::openprot_tlv::TlvAny>() { + if let Some(val) = item.cast::() { + return Some(Rsa4096FingerprintDigest::Sha2256(val)); + } + if let Some(val) = item.cast::() { + return Some(Rsa4096FingerprintDigest::Sha2384(val)); + } + } + None + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct HybridP256SlhDsaFingerprint {} +const _: () = { + assert!(::core::mem::align_of::() <= 4); + assert!(::core::mem::size_of::() == 0usize); +}; +impl ::core::default::Default for HybridP256SlhDsaFingerprint { + fn default() -> Self { + ::zerocopy::FromZeros::new_zeroed() + } +} +impl ::openprot_tlv::TlvObject for HybridP256SlhDsaFingerprint { + type Extension = ::openprot_tlv::TlvData; + const TAG: u32 = u32::from_le_bytes(*b"H+S2"); + fn make_ext<'a>(_header: &::openprot_tlv::TlvHeader, extra: &'a [u32]) -> &'a Self::Extension { + ::openprot_tlv::TlvData::overlay(extra) + } +} +pub enum HybridP256SlhDsaFingerprintDigest<'a> { + Sha2256(::openprot_tlv::TlvItem<'a, Sha2_256Digest>), + Sha2384(::openprot_tlv::TlvItem<'a, Sha2_384Digest>), +} +pub trait HybridP256SlhDsaFingerprintExt<'a> { + fn sha2_256(&self) -> Option<::openprot_tlv::TlvItem<'a, Sha2_256Digest>>; + fn sha2_384(&self) -> Option<::openprot_tlv::TlvItem<'a, Sha2_384Digest>>; + fn digest(&self) -> Option>; +} +impl<'a> HybridP256SlhDsaFingerprintExt<'a> + for ::openprot_tlv::TlvItem<'a, HybridP256SlhDsaFingerprint> +{ + #[inline(always)] + fn sha2_256(&self) -> Option<::openprot_tlv::TlvItem<'a, Sha2_256Digest>> { + ::openprot_tlv::TlvQuery::ext(self) + .iter::() + .next() + } + #[inline(always)] + fn sha2_384(&self) -> Option<::openprot_tlv::TlvItem<'a, Sha2_384Digest>> { + ::openprot_tlv::TlvQuery::ext(self) + .iter::() + .next() + } + #[inline(always)] + fn digest(&self) -> Option> { + use ::openprot_tlv::TlvQuery; + for item in ::openprot_tlv::TlvQuery::ext(self).iter::<::openprot_tlv::TlvAny>() { + if let Some(val) = item.cast::() { + return Some(HybridP256SlhDsaFingerprintDigest::Sha2256(val)); + } + if let Some(val) = item.cast::() { + return Some(HybridP256SlhDsaFingerprintDigest::Sha2384(val)); + } + } + None + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct MlDsa44Fingerprint {} +const _: () = { + assert!(::core::mem::align_of::() <= 4); + assert!(::core::mem::size_of::() == 0usize); +}; +impl ::core::default::Default for MlDsa44Fingerprint { + fn default() -> Self { + ::zerocopy::FromZeros::new_zeroed() + } +} +impl ::openprot_tlv::TlvObject for MlDsa44Fingerprint { + type Extension = ::openprot_tlv::TlvData; + const TAG: u32 = u32::from_le_bytes(*b"ML44"); + fn make_ext<'a>(_header: &::openprot_tlv::TlvHeader, extra: &'a [u32]) -> &'a Self::Extension { + ::openprot_tlv::TlvData::overlay(extra) + } +} +pub enum MlDsa44FingerprintDigest<'a> { + Sha2256(::openprot_tlv::TlvItem<'a, Sha2_256Digest>), + Sha2384(::openprot_tlv::TlvItem<'a, Sha2_384Digest>), +} +pub trait MlDsa44FingerprintExt<'a> { + fn sha2_256(&self) -> Option<::openprot_tlv::TlvItem<'a, Sha2_256Digest>>; + fn sha2_384(&self) -> Option<::openprot_tlv::TlvItem<'a, Sha2_384Digest>>; + fn digest(&self) -> Option>; +} +impl<'a> MlDsa44FingerprintExt<'a> for ::openprot_tlv::TlvItem<'a, MlDsa44Fingerprint> { + #[inline(always)] + fn sha2_256(&self) -> Option<::openprot_tlv::TlvItem<'a, Sha2_256Digest>> { + ::openprot_tlv::TlvQuery::ext(self) + .iter::() + .next() + } + #[inline(always)] + fn sha2_384(&self) -> Option<::openprot_tlv::TlvItem<'a, Sha2_384Digest>> { + ::openprot_tlv::TlvQuery::ext(self) + .iter::() + .next() + } + #[inline(always)] + fn digest(&self) -> Option> { + use ::openprot_tlv::TlvQuery; + for item in ::openprot_tlv::TlvQuery::ext(self).iter::<::openprot_tlv::TlvAny>() { + if let Some(val) = item.cast::() { + return Some(MlDsa44FingerprintDigest::Sha2256(val)); + } + if let Some(val) = item.cast::() { + return Some(MlDsa44FingerprintDigest::Sha2384(val)); + } + } + None + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct HybridP256MlDsa44Fingerprint {} +const _: () = { + assert!(::core::mem::align_of::() <= 4); + assert!(::core::mem::size_of::() == 0usize); +}; +impl ::core::default::Default for HybridP256MlDsa44Fingerprint { + fn default() -> Self { + ::zerocopy::FromZeros::new_zeroed() + } +} +impl ::openprot_tlv::TlvObject for HybridP256MlDsa44Fingerprint { + type Extension = ::openprot_tlv::TlvData; + const TAG: u32 = u32::from_le_bytes(*b"M4P2"); + fn make_ext<'a>(_header: &::openprot_tlv::TlvHeader, extra: &'a [u32]) -> &'a Self::Extension { + ::openprot_tlv::TlvData::overlay(extra) + } +} +pub enum HybridP256MlDsa44FingerprintDigest<'a> { + Sha2256(::openprot_tlv::TlvItem<'a, Sha2_256Digest>), + Sha2384(::openprot_tlv::TlvItem<'a, Sha2_384Digest>), +} +pub trait HybridP256MlDsa44FingerprintExt<'a> { + fn sha2_256(&self) -> Option<::openprot_tlv::TlvItem<'a, Sha2_256Digest>>; + fn sha2_384(&self) -> Option<::openprot_tlv::TlvItem<'a, Sha2_384Digest>>; + fn digest(&self) -> Option>; +} +impl<'a> HybridP256MlDsa44FingerprintExt<'a> + for ::openprot_tlv::TlvItem<'a, HybridP256MlDsa44Fingerprint> +{ + #[inline(always)] + fn sha2_256(&self) -> Option<::openprot_tlv::TlvItem<'a, Sha2_256Digest>> { + ::openprot_tlv::TlvQuery::ext(self) + .iter::() + .next() + } + #[inline(always)] + fn sha2_384(&self) -> Option<::openprot_tlv::TlvItem<'a, Sha2_384Digest>> { + ::openprot_tlv::TlvQuery::ext(self) + .iter::() + .next() + } + #[inline(always)] + fn digest(&self) -> Option> { + use ::openprot_tlv::TlvQuery; + for item in ::openprot_tlv::TlvQuery::ext(self).iter::<::openprot_tlv::TlvAny>() { + if let Some(val) = item.cast::() { + return Some(HybridP256MlDsa44FingerprintDigest::Sha2256(val)); + } + if let Some(val) = item.cast::() { + return Some(HybridP256MlDsa44FingerprintDigest::Sha2384(val)); + } + } + None + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct KeyFingerprint { + pub key_domain: KeyDomain, +} +const _: () = { + assert!(::core::mem::offset_of!(KeyFingerprint, key_domain) == 0usize); + assert!(::core::mem::align_of::() <= 4); + assert!(::core::mem::size_of::() == 4usize); +}; +impl ::core::default::Default for KeyFingerprint { + fn default() -> Self { + ::zerocopy::FromZeros::new_zeroed() + } +} +impl ::openprot_tlv::TlvObject for KeyFingerprint { + type Extension = ::openprot_tlv::TlvData; + const TAG: u32 = u32::from_le_bytes(*b"KFIP"); + fn make_ext<'a>(_header: &::openprot_tlv::TlvHeader, extra: &'a [u32]) -> &'a Self::Extension { + ::openprot_tlv::TlvData::overlay(extra) + } +} +pub enum KeyFingerprintKeyType<'a> { + P256(::openprot_tlv::TlvItem<'a, P256Fingerprint>), + Slhdsa(::openprot_tlv::TlvItem<'a, SlhDsaFingerprint>), + Rsa2048(::openprot_tlv::TlvItem<'a, Rsa2048Fingerprint>), + Rsa3072(::openprot_tlv::TlvItem<'a, Rsa3072Fingerprint>), + Rsa4096(::openprot_tlv::TlvItem<'a, Rsa4096Fingerprint>), + HybridP256Slhdsa(::openprot_tlv::TlvItem<'a, HybridP256SlhDsaFingerprint>), + Mldsa(::openprot_tlv::TlvItem<'a, MlDsa44Fingerprint>), + HybridP256Mldsa(::openprot_tlv::TlvItem<'a, HybridP256MlDsa44Fingerprint>), +} +pub trait KeyFingerprintExt<'a> { + fn p256(&self) -> Option<::openprot_tlv::TlvItem<'a, P256Fingerprint>>; + fn slhdsa(&self) -> Option<::openprot_tlv::TlvItem<'a, SlhDsaFingerprint>>; + fn rsa2048(&self) -> Option<::openprot_tlv::TlvItem<'a, Rsa2048Fingerprint>>; + fn rsa3072(&self) -> Option<::openprot_tlv::TlvItem<'a, Rsa3072Fingerprint>>; + fn rsa4096(&self) -> Option<::openprot_tlv::TlvItem<'a, Rsa4096Fingerprint>>; + fn hybrid_p256_slhdsa( + &self, + ) -> Option<::openprot_tlv::TlvItem<'a, HybridP256SlhDsaFingerprint>>; + fn mldsa(&self) -> Option<::openprot_tlv::TlvItem<'a, MlDsa44Fingerprint>>; + fn hybrid_p256_mldsa( + &self, + ) -> Option<::openprot_tlv::TlvItem<'a, HybridP256MlDsa44Fingerprint>>; + fn key_type(&self) -> Option>; +} +impl<'a> KeyFingerprintExt<'a> for ::openprot_tlv::TlvItem<'a, KeyFingerprint> { + #[inline(always)] + fn p256(&self) -> Option<::openprot_tlv::TlvItem<'a, P256Fingerprint>> { + ::openprot_tlv::TlvQuery::ext(self) + .iter::() + .next() + } + #[inline(always)] + fn slhdsa(&self) -> Option<::openprot_tlv::TlvItem<'a, SlhDsaFingerprint>> { + ::openprot_tlv::TlvQuery::ext(self) + .iter::() + .next() + } + #[inline(always)] + fn rsa2048(&self) -> Option<::openprot_tlv::TlvItem<'a, Rsa2048Fingerprint>> { + ::openprot_tlv::TlvQuery::ext(self) + .iter::() + .next() + } + #[inline(always)] + fn rsa3072(&self) -> Option<::openprot_tlv::TlvItem<'a, Rsa3072Fingerprint>> { + ::openprot_tlv::TlvQuery::ext(self) + .iter::() + .next() + } + #[inline(always)] + fn rsa4096(&self) -> Option<::openprot_tlv::TlvItem<'a, Rsa4096Fingerprint>> { + ::openprot_tlv::TlvQuery::ext(self) + .iter::() + .next() + } + #[inline(always)] + fn hybrid_p256_slhdsa( + &self, + ) -> Option<::openprot_tlv::TlvItem<'a, HybridP256SlhDsaFingerprint>> { + ::openprot_tlv::TlvQuery::ext(self) + .iter::() + .next() + } + #[inline(always)] + fn mldsa(&self) -> Option<::openprot_tlv::TlvItem<'a, MlDsa44Fingerprint>> { + ::openprot_tlv::TlvQuery::ext(self) + .iter::() + .next() + } + #[inline(always)] + fn hybrid_p256_mldsa( + &self, + ) -> Option<::openprot_tlv::TlvItem<'a, HybridP256MlDsa44Fingerprint>> { + ::openprot_tlv::TlvQuery::ext(self) + .iter::() + .next() + } + #[inline(always)] + fn key_type(&self) -> Option> { + use ::openprot_tlv::TlvQuery; + for item in ::openprot_tlv::TlvQuery::ext(self).iter::<::openprot_tlv::TlvAny>() { + if let Some(val) = item.cast::() { + return Some(KeyFingerprintKeyType::P256(val)); + } + if let Some(val) = item.cast::() { + return Some(KeyFingerprintKeyType::Slhdsa(val)); + } + if let Some(val) = item.cast::() { + return Some(KeyFingerprintKeyType::Rsa2048(val)); + } + if let Some(val) = item.cast::() { + return Some(KeyFingerprintKeyType::Rsa3072(val)); + } + if let Some(val) = item.cast::() { + return Some(KeyFingerprintKeyType::Rsa4096(val)); + } + if let Some(val) = item.cast::() { + return Some(KeyFingerprintKeyType::HybridP256Slhdsa(val)); + } + if let Some(val) = item.cast::() { + return Some(KeyFingerprintKeyType::Mldsa(val)); + } + if let Some(val) = item.cast::() { + return Some(KeyFingerprintKeyType::HybridP256Mldsa(val)); + } + } + None + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct Kmac256Seal { + pub data: [u32; 8], +} +const _: () = { + assert!(::core::mem::offset_of!(Kmac256Seal, data) == 0usize); + assert!(::core::mem::align_of::() <= 4); + assert!(::core::mem::size_of::() == 32usize); +}; +impl ::core::default::Default for Kmac256Seal { + fn default() -> Self { + ::zerocopy::FromZeros::new_zeroed() + } +} +impl ::openprot_tlv::TlvObject for Kmac256Seal { + type Extension = [u8]; + const TAG: u32 = u32::from_le_bytes(*b"KMSL"); + fn make_ext<'a>(header: &::openprot_tlv::TlvHeader, extra: &'a [u32]) -> &'a Self::Extension { + ::openprot_tlv::unpad(header, extra) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct KRSeal {} +const _: () = { + assert!(::core::mem::align_of::() <= 4); + assert!(::core::mem::size_of::() == 0usize); +}; +impl ::core::default::Default for KRSeal { + fn default() -> Self { + ::zerocopy::FromZeros::new_zeroed() + } +} +impl ::openprot_tlv::TlvObject for KRSeal { + type Extension = ::openprot_tlv::TlvData; + const TAG: u32 = u32::from_le_bytes(*b"SEAL"); + fn make_ext<'a>(_header: &::openprot_tlv::TlvHeader, extra: &'a [u32]) -> &'a Self::Extension { + ::openprot_tlv::TlvData::overlay(extra) + } +} +pub enum KRSealSeal<'a> { + Kmsl(::openprot_tlv::TlvItem<'a, Kmac256Seal>), +} +pub trait KRSealExt<'a> { + fn kmsl(&self) -> Option<::openprot_tlv::TlvItem<'a, Kmac256Seal>>; + fn seal(&self) -> Option>; +} +impl<'a> KRSealExt<'a> for ::openprot_tlv::TlvItem<'a, KRSeal> { + #[inline(always)] + fn kmsl(&self) -> Option<::openprot_tlv::TlvItem<'a, Kmac256Seal>> { + ::openprot_tlv::TlvQuery::ext(self) + .iter::() + .next() + } + #[inline(always)] + fn seal(&self) -> Option> { + use ::openprot_tlv::TlvQuery; + for item in ::openprot_tlv::TlvQuery::ext(self).iter::<::openprot_tlv::TlvAny>() { + if let Some(val) = item.cast::() { + return Some(KRSealSeal::Kmsl(val)); + } + } + None + } +} diff --git a/shared/openprot/proto/identity.proto b/shared/openprot/proto/identity.proto new file mode 100644 index 00000000..d186c0e9 --- /dev/null +++ b/shared/openprot/proto/identity.proto @@ -0,0 +1,29 @@ +// Copyright 2026 Google LLC +// +// 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. + +syntax = "proto3"; + +package openprot.identity; + +import "shared/tlv/options.proto"; + +// An image vendor identifier assigned by OpenPRoT (8-byte fixed string). +message ImageVendor { + string name = 1 [(openprot.tlv.offset) = 0, (openprot.tlv.type) = "[u8; 8]"]; +} + +// A vendor-specific product family identifier (8-byte fixed string). +message ImageFamily { + string name = 1 [(openprot.tlv.offset) = 0, (openprot.tlv.type) = "[u8; 8]"]; +} diff --git a/shared/openprot/proto/identity_tlv.rs b/shared/openprot/proto/identity_tlv.rs new file mode 100644 index 00000000..8ee6a2ee --- /dev/null +++ b/shared/openprot/proto/identity_tlv.rs @@ -0,0 +1,60 @@ +// Copyright 2026 Google LLC +// +// 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. + +// Automatically generated by tlv_codegen. DO NOT EDIT. +// To regenerate, run: +// bazel run //shared/openprot/proto:identity_tlv_update + +#![no_std] +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] +#![allow(unused_imports)] +#![allow(dead_code)] + +use bitfield_struct::bitfield; +use openprot_tlv::TlvFlags; +use openprot_tlv::TlvHeader; +use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct ImageVendor { + pub name: [u8; 8], +} +const _: () = { + assert!(::core::mem::offset_of!(ImageVendor, name) == 0usize); + assert!(::core::mem::align_of::() <= 4); + assert!(::core::mem::size_of::() == 8usize); +}; +impl ::core::default::Default for ImageVendor { + fn default() -> Self { + ::zerocopy::FromZeros::new_zeroed() + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct ImageFamily { + pub name: [u8; 8], +} +const _: () = { + assert!(::core::mem::offset_of!(ImageFamily, name) == 0usize); + assert!(::core::mem::align_of::() <= 4); + assert!(::core::mem::size_of::() == 8usize); +}; +impl ::core::default::Default for ImageFamily { + fn default() -> Self { + ::zerocopy::FromZeros::new_zeroed() + } +} diff --git a/shared/openprot/proto/key_rotation.proto b/shared/openprot/proto/key_rotation.proto new file mode 100644 index 00000000..479300af --- /dev/null +++ b/shared/openprot/proto/key_rotation.proto @@ -0,0 +1,70 @@ +// Copyright 2026 Google LLC +// +// 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. + +syntax = "proto3"; + +package openprot.key_rotation; + +import "shared/tlv/options.proto"; +import "shared/openprot/proto/crypto.proto"; +import "shared/openprot/proto/identity.proto"; + +// A signed key rotation record descriptor container (communication, host tools, +// etc.). +message SignedDescriptor { + option (openprot.tlv.tlv_fourcc) = "KRSD"; + + Descriptor descriptor = 1; + repeated openprot.crypto.SignatureContainer signature = 2; +} + +// A sealed key rotation record descriptor container (embedded storage). +message SealedDescriptor { + option (openprot.tlv.tlv_fourcc) = "KRSL"; + + Descriptor descriptor = 1; + openprot.crypto.KRSeal seal = 2; +} + +message LegacyImageFamily { + option (openprot.tlv.tlv_fourcc) = "LGIF"; + uint32 family = 1 [(openprot.tlv.offset) = 0]; +} + +// The key rotation record descriptor. +message Descriptor { + option (openprot.tlv.tlv_fourcc) = "KROT"; + + uint32 version = 1 [(openprot.tlv.offset) = 0]; + openprot.identity.ImageVendor image_vendor = 2 [(openprot.tlv.offset) = 4]; + openprot.identity.ImageFamily image_family = 3 [(openprot.tlv.offset) = 12]; + + optional LegacyImageFamily legacy_image_family = 4; + + // TLV extensions. + repeated openprot.crypto.KeyFingerprint fingerprint = 5; + repeated ImgDescriptorDigest img_descriptor_digest = 6; +} + +// A digest of an allowed image descriptor. +// A list of multiple digests is represented by repeating this tag. +message ImgDescriptorDigest { + option (openprot.tlv.tlv_fourcc) = "IDDG"; + + // Digest. + oneof digest { + openprot.crypto.Sha2_256Digest sha2_256 = 1; + openprot.crypto.Sha2_384Digest sha2_384 = 2; + } +} diff --git a/shared/openprot/proto/key_rotation_tlv.rs b/shared/openprot/proto/key_rotation_tlv.rs new file mode 100644 index 00000000..551574fc --- /dev/null +++ b/shared/openprot/proto/key_rotation_tlv.rs @@ -0,0 +1,243 @@ +// Copyright 2026 Google LLC +// +// 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. + +// Automatically generated by tlv_codegen. DO NOT EDIT. +// To regenerate, run: +// bazel run //shared/openprot/proto:key_rotation_tlv_update + +#![no_std] +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] +#![allow(unused_imports)] +#![allow(dead_code)] + +use bitfield_struct::bitfield; +pub use crypto_tlv::*; +pub use identity_tlv::*; +use openprot_tlv::TlvFlags; +use openprot_tlv::TlvHeader; +use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct SignedDescriptor {} +const _: () = { + assert!(::core::mem::align_of::() <= 4); + assert!(::core::mem::size_of::() == 0usize); +}; +impl ::core::default::Default for SignedDescriptor { + fn default() -> Self { + ::zerocopy::FromZeros::new_zeroed() + } +} +impl ::openprot_tlv::TlvObject for SignedDescriptor { + type Extension = ::openprot_tlv::TlvData; + const TAG: u32 = u32::from_le_bytes(*b"KRSD"); + fn make_ext<'a>(_header: &::openprot_tlv::TlvHeader, extra: &'a [u32]) -> &'a Self::Extension { + ::openprot_tlv::TlvData::overlay(extra) + } +} +pub trait SignedDescriptorExt<'a> { + fn descriptor(&self) -> Option<::openprot_tlv::TlvItem<'a, Descriptor>>; + fn signature(&self) -> ::openprot_tlv::TlvIterator<'a, SignatureContainer>; +} +impl<'a> SignedDescriptorExt<'a> for ::openprot_tlv::TlvItem<'a, SignedDescriptor> { + #[inline(always)] + fn descriptor(&self) -> Option<::openprot_tlv::TlvItem<'a, Descriptor>> { + ::openprot_tlv::TlvQuery::ext(self) + .iter::() + .next() + } + #[inline(always)] + fn signature(&self) -> ::openprot_tlv::TlvIterator<'a, SignatureContainer> { + ::openprot_tlv::TlvQuery::ext(self).iter::() + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct SealedDescriptor {} +const _: () = { + assert!(::core::mem::align_of::() <= 4); + assert!(::core::mem::size_of::() == 0usize); +}; +impl ::core::default::Default for SealedDescriptor { + fn default() -> Self { + ::zerocopy::FromZeros::new_zeroed() + } +} +impl ::openprot_tlv::TlvObject for SealedDescriptor { + type Extension = ::openprot_tlv::TlvData; + const TAG: u32 = u32::from_le_bytes(*b"KRSL"); + fn make_ext<'a>(_header: &::openprot_tlv::TlvHeader, extra: &'a [u32]) -> &'a Self::Extension { + ::openprot_tlv::TlvData::overlay(extra) + } +} +pub trait SealedDescriptorExt<'a> { + fn descriptor(&self) -> Option<::openprot_tlv::TlvItem<'a, Descriptor>>; + fn seal(&self) -> Option<::openprot_tlv::TlvItem<'a, KRSeal>>; +} +impl<'a> SealedDescriptorExt<'a> for ::openprot_tlv::TlvItem<'a, SealedDescriptor> { + #[inline(always)] + fn descriptor(&self) -> Option<::openprot_tlv::TlvItem<'a, Descriptor>> { + ::openprot_tlv::TlvQuery::ext(self) + .iter::() + .next() + } + #[inline(always)] + fn seal(&self) -> Option<::openprot_tlv::TlvItem<'a, KRSeal>> { + ::openprot_tlv::TlvQuery::ext(self).iter::().next() + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct LegacyImageFamily { + pub family: u32, +} +const _: () = { + assert!(::core::mem::offset_of!(LegacyImageFamily, family) == 0usize); + assert!(::core::mem::align_of::() <= 4); + assert!(::core::mem::size_of::() == 4usize); +}; +impl ::core::default::Default for LegacyImageFamily { + fn default() -> Self { + ::zerocopy::FromZeros::new_zeroed() + } +} +impl ::openprot_tlv::TlvObject for LegacyImageFamily { + type Extension = [u8]; + const TAG: u32 = u32::from_le_bytes(*b"LGIF"); + fn make_ext<'a>(header: &::openprot_tlv::TlvHeader, extra: &'a [u32]) -> &'a Self::Extension { + ::openprot_tlv::unpad(header, extra) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct Descriptor { + pub version: u32, + pub image_vendor: ImageVendor, + pub image_family: ImageFamily, +} +const _: () = { + assert!(::core::mem::offset_of!(Descriptor, version) == 0usize); + assert!(::core::mem::offset_of!(Descriptor, image_vendor) == 4usize); + assert!(::core::mem::offset_of!(Descriptor, image_family) == 12usize); + assert!(::core::mem::align_of::() <= 4); + assert!(::core::mem::size_of::() == 20usize); +}; +impl ::core::default::Default for Descriptor { + fn default() -> Self { + ::zerocopy::FromZeros::new_zeroed() + } +} +impl ::openprot_tlv::TlvObject for Descriptor { + type Extension = ::openprot_tlv::TlvData; + const TAG: u32 = u32::from_le_bytes(*b"KROT"); + fn make_ext<'a>(_header: &::openprot_tlv::TlvHeader, extra: &'a [u32]) -> &'a Self::Extension { + ::openprot_tlv::TlvData::overlay(extra) + } +} +pub enum DescriptorLegacyImageFamily<'a> { + LegacyImageFamily(::openprot_tlv::TlvItem<'a, LegacyImageFamily>), +} +pub trait DescriptorExt<'a> { + fn legacy_image_family(&self) -> Option<::openprot_tlv::TlvItem<'a, LegacyImageFamily>>; + fn fingerprint(&self) -> ::openprot_tlv::TlvIterator<'a, KeyFingerprint>; + fn img_descriptor_digest(&self) -> ::openprot_tlv::TlvIterator<'a, ImgDescriptorDigest>; + fn _legacy_image_family(&self) -> Option>; +} +impl<'a> DescriptorExt<'a> for ::openprot_tlv::TlvItem<'a, Descriptor> { + #[inline(always)] + fn legacy_image_family(&self) -> Option<::openprot_tlv::TlvItem<'a, LegacyImageFamily>> { + ::openprot_tlv::TlvQuery::ext(self) + .iter::() + .next() + } + #[inline(always)] + fn fingerprint(&self) -> ::openprot_tlv::TlvIterator<'a, KeyFingerprint> { + ::openprot_tlv::TlvQuery::ext(self).iter::() + } + #[inline(always)] + fn img_descriptor_digest(&self) -> ::openprot_tlv::TlvIterator<'a, ImgDescriptorDigest> { + ::openprot_tlv::TlvQuery::ext(self).iter::() + } + #[inline(always)] + fn _legacy_image_family(&self) -> Option> { + use ::openprot_tlv::TlvQuery; + for item in ::openprot_tlv::TlvQuery::ext(self).iter::<::openprot_tlv::TlvAny>() { + if let Some(val) = item.cast::() { + return Some(DescriptorLegacyImageFamily::LegacyImageFamily(val)); + } + } + None + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct ImgDescriptorDigest {} +const _: () = { + assert!(::core::mem::align_of::() <= 4); + assert!(::core::mem::size_of::() == 0usize); +}; +impl ::core::default::Default for ImgDescriptorDigest { + fn default() -> Self { + ::zerocopy::FromZeros::new_zeroed() + } +} +impl ::openprot_tlv::TlvObject for ImgDescriptorDigest { + type Extension = ::openprot_tlv::TlvData; + const TAG: u32 = u32::from_le_bytes(*b"IDDG"); + fn make_ext<'a>(_header: &::openprot_tlv::TlvHeader, extra: &'a [u32]) -> &'a Self::Extension { + ::openprot_tlv::TlvData::overlay(extra) + } +} +pub enum ImgDescriptorDigestDigest<'a> { + Sha2256(::openprot_tlv::TlvItem<'a, Sha2_256Digest>), + Sha2384(::openprot_tlv::TlvItem<'a, Sha2_384Digest>), +} +pub trait ImgDescriptorDigestExt<'a> { + fn sha2_256(&self) -> Option<::openprot_tlv::TlvItem<'a, Sha2_256Digest>>; + fn sha2_384(&self) -> Option<::openprot_tlv::TlvItem<'a, Sha2_384Digest>>; + fn digest(&self) -> Option>; +} +impl<'a> ImgDescriptorDigestExt<'a> for ::openprot_tlv::TlvItem<'a, ImgDescriptorDigest> { + #[inline(always)] + fn sha2_256(&self) -> Option<::openprot_tlv::TlvItem<'a, Sha2_256Digest>> { + ::openprot_tlv::TlvQuery::ext(self) + .iter::() + .next() + } + #[inline(always)] + fn sha2_384(&self) -> Option<::openprot_tlv::TlvItem<'a, Sha2_384Digest>> { + ::openprot_tlv::TlvQuery::ext(self) + .iter::() + .next() + } + #[inline(always)] + fn digest(&self) -> Option> { + use ::openprot_tlv::TlvQuery; + for item in ::openprot_tlv::TlvQuery::ext(self).iter::<::openprot_tlv::TlvAny>() { + if let Some(val) = item.cast::() { + return Some(ImgDescriptorDigestDigest::Sha2256(val)); + } + if let Some(val) = item.cast::() { + return Some(ImgDescriptorDigestDigest::Sha2384(val)); + } + } + None + } +} diff --git a/shared/openprot/proto/lib.rs b/shared/openprot/proto/lib.rs new file mode 100644 index 00000000..6fc602e8 --- /dev/null +++ b/shared/openprot/proto/lib.rs @@ -0,0 +1,194 @@ +// Copyright 2026 Google LLC +// +// 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. + +#![cfg_attr(not(test), no_std)] + +pub mod chipid { + pub use chipid_tlv::*; +} + +pub mod crypto { + pub use crypto_tlv::*; +} + +pub mod identity { + pub use identity_tlv::*; +} + +pub mod key_rotation { + pub use key_rotation_tlv::*; +} + +#[cfg(test)] +extern crate alloc; + +#[cfg(test)] +mod tests { + use super::*; + use alloc::string::ToString; + use alloc::vec; + use key_rotation::*; + use openprot_tlv::TlvData; + + #[test] + fn test_key_rotation_proto_macro_conv() { + let signed_desc = protobuf::proto!(key_rotation_rust_proto::SignedDescriptor { + descriptor: key_rotation_rust_proto::Descriptor { + version: 2, + image_vendor: identity_rust_proto::ImageVendor { + name: "GOOGLE".to_string(), + }, + image_family: identity_rust_proto::ImageFamily { + name: "FAM1".to_string(), + }, + legacy_image_family: key_rotation_rust_proto::LegacyImageFamily { family: 1 }, + fingerprint: [ + crypto_rust_proto::KeyFingerprint { + key_domain: crypto_rust_proto::KeyDomain::PayloadProd, + p256: crypto_rust_proto::P256Fingerprint { + sha2_256: crypto_rust_proto::Sha2_256Digest { + data: vec![ + 1, 2, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ], + } + } + }, + crypto_rust_proto::KeyFingerprint { + key_domain: crypto_rust_proto::KeyDomain::PayloadProd, + slhdsa: crypto_rust_proto::SlhDsaFingerprint { + sha2_256: crypto_rust_proto::Sha2_256Digest { + data: vec![ + 5, 6, 7, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ], + } + } + }, + ], + }, + signature: [crypto_rust_proto::SignatureContainer { + signature: crypto_rust_proto::Signature { + p256: crypto_rust_proto::P256Signature { + r: vec![ + 9, 8, 7, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + ], + s: vec![ + 5, 4, 3, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + ], + } + } + }], + }); + + let tlv_words = openprot_tlv_proto_conv::message_to_tlv( + &key_rotation_rust_proto::__unstable::SHARED_OPENPROT_PROTO_KEY_ROTATION_DESCRIPTOR_INFO, + &signed_desc, + ) + .expect("Failed to convert SignedDescriptor to TLV"); + + let tlv_data = TlvData::overlay(&tlv_words); + let signed_item = tlv_data.iter::().next().unwrap(); + let desc_item = signed_item.descriptor().unwrap(); + assert_eq!(desc_item.data.version, 2); + assert_eq!(desc_item.data.image_vendor.name, *b"GOOGLE\0\0"); + assert_eq!(desc_item.data.image_family.name, *b"FAM1\0\0\0\0"); + let legacy_family = desc_item.legacy_image_family().unwrap(); + assert_eq!(legacy_family.data.family, 1); + + // Get fingerprint + let mut fp_iter = desc_item.fingerprint(); + let kf_item = fp_iter.next().unwrap(); + assert_eq!(kf_item.data.key_domain, KeyDomain::PAYLOAD_PROD); + match kf_item.key_type().unwrap() { + KeyFingerprintKeyType::P256(p256_item) => match p256_item.digest().unwrap() { + P256FingerprintDigest::Sha2256(sha_item) => { + assert_eq!(sha_item.data.data[0..4], [1, 2, 3, 4]); + } + _ => panic!("Expected Sha256"), + }, + _ => panic!("Expected P256"), + } + + let kf_item_slh = fp_iter.next().unwrap(); + assert_eq!(kf_item_slh.data.key_domain, KeyDomain::PAYLOAD_PROD); + match kf_item_slh.key_type().unwrap() { + KeyFingerprintKeyType::Slhdsa(slh_item) => match slh_item.digest().unwrap() { + SlhDsaFingerprintDigest::Sha2256(sha_item) => { + assert_eq!(sha_item.data.data[0..4], [5, 6, 7, 8]); + } + _ => panic!("Expected Sha256"), + }, + _ => panic!("Expected Slhdsa"), + } + + // Verify signature + let mut sig_iter = signed_item.signature(); + let sigc_item = sig_iter.next().unwrap(); + let sig_item = sigc_item.signature().unwrap(); + match sig_item.signature().unwrap() { + SignatureSignature::P256(item) => { + assert_eq!(item.data.r[0..4], [0x06070809, 0, 0, 0]); + assert_eq!(item.data.s[0..4], [0x02030405, 0, 0, 0]); + } + _ => panic!("Expected P256"), + } + } + + #[test] + fn test_key_rotation_sealed_descriptor_proto_macro_conv() { + let sealed_desc = protobuf::proto!(key_rotation_rust_proto::SealedDescriptor { + descriptor: key_rotation_rust_proto::Descriptor { + version: 1, + image_vendor: identity_rust_proto::ImageVendor { + name: "GOOGLE".to_string(), + }, + image_family: identity_rust_proto::ImageFamily { + name: "FAM1".to_string(), + }, + }, + seal: crypto_rust_proto::KRSeal { + kmsl: crypto_rust_proto::Kmac256Seal { + data: vec![ + 1, 2, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, + ], + } + }, + }); + + let tlv_words = openprot_tlv_proto_conv::message_to_tlv( + &key_rotation_rust_proto::__unstable::SHARED_OPENPROT_PROTO_KEY_ROTATION_DESCRIPTOR_INFO, + &sealed_desc, + ) + .expect("Failed to convert SealedDescriptor to TLV"); + + let tlv_data = TlvData::overlay(&tlv_words); + let sealed_item = tlv_data.iter::().next().unwrap(); + let desc_item = sealed_item.descriptor().unwrap(); + assert_eq!(desc_item.data.version, 1); + assert_eq!(desc_item.data.image_vendor.name, *b"GOOGLE\0\0"); + assert_eq!(desc_item.data.image_family.name, *b"FAM1\0\0\0\0"); + assert!(desc_item.legacy_image_family().is_none()); + + let seal_item = sealed_item.seal().unwrap(); + match seal_item.seal().unwrap() { + KRSealSeal::Kmsl(kmsl_item) => { + assert_eq!(kmsl_item.data.data[0], 0x04030201); + } + } + } +} diff --git a/shared/proto/BUILD.bazel b/shared/proto/BUILD.bazel new file mode 100644 index 00000000..02c35397 --- /dev/null +++ b/shared/proto/BUILD.bazel @@ -0,0 +1,49 @@ +# Copyright 2026 Google LLC +# +# 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. + +load("@protobuf//rust:defs.bzl", "rust_proto_library") +load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") + +rust_library( + name = "reader", + srcs = ["reader.rs"], + crate_name = "openprot_proto_reader", + edition = "2024", + visibility = ["//visibility:public"], +) + +rust_test( + name = "reader_test", + crate = ":reader", + edition = "2024", +) + +rust_library( + name = "reflect", + srcs = ["reflect.rs"], + crate_name = "openprot_proto_reflect", + edition = "2024", + visibility = ["//visibility:public"], + deps = [ + ":descriptor_rust_proto", + "@protobuf//rust:protobuf", + ], +) + +rust_proto_library( + name = "descriptor_rust_proto", + tags = ["manual"], + visibility = ["//visibility:public"], + deps = ["@protobuf//:descriptor_proto"], +) diff --git a/shared/proto/reader.rs b/shared/proto/reader.rs new file mode 100644 index 00000000..f973a8b4 --- /dev/null +++ b/shared/proto/reader.rs @@ -0,0 +1,581 @@ +// Copyright 2026 Google LLC +// +// 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. + +use std::fmt; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ProtoInputError { + UnexpectedEof, + VarintOverflow, + UnsupportedWireType(u8), +} + +impl fmt::Display for ProtoInputError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::UnexpectedEof => write!(f, "Unexpected EOF while parsing protobuf stream"), + Self::VarintOverflow => write!(f, "Varint overflow (greater than 64 bits)"), + Self::UnsupportedWireType(wt) => write!(f, "Unsupported protobuf wire type: {}", wt), + } + } +} + +impl std::error::Error for ProtoInputError {} + +/// Supported protobuf field types for decoding, represented as an open enum. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct FieldType(pub i32); + +#[allow(non_upper_case_globals)] +impl FieldType { + pub const Double: FieldType = FieldType(1); + pub const Float: FieldType = FieldType(2); + pub const Int64: FieldType = FieldType(3); + pub const Uint64: FieldType = FieldType(4); + pub const Int32: FieldType = FieldType(5); + pub const Fixed64: FieldType = FieldType(6); + pub const Fixed32: FieldType = FieldType(7); + pub const Bool: FieldType = FieldType(8); + pub const String: FieldType = FieldType(9); + pub const Group: FieldType = FieldType(10); + pub const Message: FieldType = FieldType(11); + pub const Bytes: FieldType = FieldType(12); + pub const Uint32: FieldType = FieldType(13); + pub const Enum: FieldType = FieldType(14); + pub const Sfixed32: FieldType = FieldType(15); + pub const Sfixed64: FieldType = FieldType(16); + pub const Sint32: FieldType = FieldType(17); + pub const Sint64: FieldType = FieldType(18); +} + +/// A parsed protobuf wire-format record containing its typed value. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Record<'a> { + Varint(u64), + Fixed64(u64), + LengthDelimited(&'a [u8]), + Fixed32(u32), +} + +impl<'a> Record<'a> { + /// Returns the raw wire type u8 value of this record. + pub fn wire_type_u8(&self) -> u8 { + match self { + Self::Varint(_) => 0, + Self::Fixed64(_) => 1, + Self::LengthDelimited(_) => 2, + Self::Fixed32(_) => 5, + } + } + + /// Decodes this record's value as a `u64` according to the given `FieldType`. + /// + /// This method automatically handles type-specific decoding rules, including: + /// - **Zig-zag decoding** for signed varints (`Sint32` and `Sint64`). + /// - **Sign-extension** for signed fixed-width integers (`Sfixed32`), mapping negative + /// values to their correct 64-bit representation (e.g., `-1` becomes `u64::MAX`). + /// + /// Returns `ProtoInputError::UnsupportedWireType` if the record's wire type + /// does not match the expected wire type for the `FieldType`. + pub fn decode_u64(&self, field_type: FieldType) -> Result { + match (self, field_type) { + // Varint wire type (0) + ( + Self::Varint(v), + FieldType::Int64 + | FieldType::Uint64 + | FieldType::Int32 + | FieldType::Uint32 + | FieldType::Bool + | FieldType::Enum, + ) => Ok(*v), + (Self::Varint(v), FieldType::Sint32 | FieldType::Sint64) => { + Ok(decode_zigzag(*v) as u64) + } + // Fixed64 wire type (1) + (Self::Fixed64(v), FieldType::Fixed64 | FieldType::Double | FieldType::Sfixed64) => { + Ok(*v) + } + // Fixed32 wire type (5) + (Self::Fixed32(v), FieldType::Fixed32 | FieldType::Float) => Ok(*v as u64), + (Self::Fixed32(v), FieldType::Sfixed32) => Ok((*v as i32) as u64), + _ => Err(ProtoInputError::UnsupportedWireType(self.wire_type_u8())), + } + } +} + +/// A lightweight, FFI-free reader for parsing protobuf wire format streams. +pub struct ProtoReader<'a> { + bytes: &'a [u8], + pos: usize, +} + +impl<'a> ProtoReader<'a> { + /// Creates a new `ProtoReader` wrapping the given byte slice. + pub fn new(bytes: &'a [u8]) -> Self { + Self { bytes, pos: 0 } + } + + /// Returns the current cursor position. + pub fn pos(&self) -> usize { + self.pos + } + + /// Set the cursor position. + pub fn set_pos(&mut self, pos: usize) { + self.pos = pos; + } + + /// Returns true if the cursor has reached the end of the byte slice. + pub fn is_eof(&self) -> bool { + self.pos >= self.bytes.len() + } + + /// Reads a Varint from the stream. + fn read_varint(&mut self) -> Result { + let mut result = 0u64; + let mut shift = 0; + while self.pos < self.bytes.len() { + let byte = self.bytes[self.pos]; + self.pos += 1; + if shift == 63 && (byte & 0x7F) > 1 { + return Err(ProtoInputError::VarintOverflow); + } + result |= ((byte & 0x7F) as u64) << shift; + if (byte & 0x80) == 0 { + return Ok(result); + } + shift += 7; + if shift >= 64 { + return Err(ProtoInputError::VarintOverflow); + } + } + Err(ProtoInputError::UnexpectedEof) + } + + /// Reads a raw slice of bytes of the specified length. + fn read_bytes(&mut self, len: usize) -> Result<&'a [u8], ProtoInputError> { + if len <= self.bytes.len() - self.pos { + let slice = &self.bytes[self.pos..self.pos + len]; + self.pos += len; + Ok(slice) + } else { + Err(ProtoInputError::UnexpectedEof) + } + } + + /// Reads a length-delimited payload. + fn read_length_delimited(&mut self) -> Result<&'a [u8], ProtoInputError> { + let len_u64 = self.read_varint()?; + let len = usize::try_from(len_u64).map_err(|_| ProtoInputError::UnexpectedEof)?; + self.read_bytes(len) + } + + /// Reads the next tag and parses its value, returning the field number and the record data. + /// + /// This guarantees the reader is always advanced to the start of the next field. + pub fn read_record(&mut self) -> Result<(u32, Record<'a>), ProtoInputError> { + let tag = self.read_varint()?; + let field_number = (tag >> 3) as u32; + let wire_type = (tag & 0x7) as u8; + + let record = match wire_type { + 0 => { + let val = self.read_varint()?; + Record::Varint(val) + } + 1 => { + let bytes = self.read_bytes(8)?; + let mut b = [0u8; 8]; + b.copy_from_slice(bytes); + Record::Fixed64(u64::from_le_bytes(b)) + } + 2 => { + let bytes = self.read_length_delimited()?; + Record::LengthDelimited(bytes) + } + 5 => { + let bytes = self.read_bytes(4)?; + let mut b = [0u8; 4]; + b.copy_from_slice(bytes); + Record::Fixed32(u32::from_le_bytes(b)) + } + other => return Err(ProtoInputError::UnsupportedWireType(other)), + }; + + Ok((field_number, record)) + } + + /// Scans the stream from the current position to find a field by its number. + /// Does not advance the caller's cursor. + pub fn find_field(&self, target_number: u32) -> Option> { + let mut reader = ProtoReader::new(self.bytes); + reader.pos = self.pos; + let mut last_match = None; + while !reader.is_eof() { + if let Ok((field_number, record)) = reader.read_record() { + if field_number == target_number { + last_match = Some(record); + } + } else { + return None; + } + } + last_match + } +} + +/// Decodes a 64-bit zig-zag encoded value back to a signed 64-bit integer. +pub fn decode_zigzag(val: u64) -> i64 { + ((val >> 1) as i64) ^ -((val & 1) as i64) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_read_varint_valid() { + // 0 + let mut reader = ProtoReader::new(&[0x00]); + assert_eq!(reader.read_varint().unwrap(), 0); + + // 127 + let mut reader = ProtoReader::new(&[0x7F]); + assert_eq!(reader.read_varint().unwrap(), 127); + + // 128 + let mut reader = ProtoReader::new(&[0x80, 0x01]); + assert_eq!(reader.read_varint().unwrap(), 128); + + // Max u64: 10 bytes: 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01 + let mut reader = + ProtoReader::new(&[0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01]); + assert_eq!(reader.read_varint().unwrap(), u64::MAX); + } + + #[test] + fn test_read_varint_overflow() { + // 10th byte has bit 1 set (represents value > 64 bits) + let mut reader = + ProtoReader::new(&[0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x02]); + assert_eq!( + reader.read_varint().unwrap_err(), + ProtoInputError::VarintOverflow + ); + + // 11th byte present + let mut reader = ProtoReader::new(&[ + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, + ]); + assert_eq!( + reader.read_varint().unwrap_err(), + ProtoInputError::VarintOverflow + ); + } + + #[test] + fn test_decode_zigzag() { + assert_eq!(decode_zigzag(0), 0); + assert_eq!(decode_zigzag(1), -1); + assert_eq!(decode_zigzag(2), 1); + assert_eq!(decode_zigzag(3), -2); + assert_eq!(decode_zigzag(4), 2); + assert_eq!(decode_zigzag(4294967294), 2147483647); + assert_eq!(decode_zigzag(4294967295), -2147483648); + assert_eq!(decode_zigzag(u64::MAX), i64::MIN); + assert_eq!(decode_zigzag(u64::MAX - 1), i64::MAX); + } + + #[test] + fn test_record_wire_type_u8() { + assert_eq!(Record::Varint(100).wire_type_u8(), 0); + assert_eq!(Record::Fixed64(0).wire_type_u8(), 1); + assert_eq!(Record::LengthDelimited(&[]).wire_type_u8(), 2); + assert_eq!(Record::Fixed32(0).wire_type_u8(), 5); + } + + #[test] + fn test_proto_reader_basic() { + let bytes = &[1, 2, 3, 4]; + let mut reader = ProtoReader::new(bytes); + assert_eq!(reader.pos(), 0); + assert!(!reader.is_eof()); + + reader.set_pos(2); + assert_eq!(reader.pos(), 2); + assert!(!reader.is_eof()); + + reader.set_pos(4); + assert_eq!(reader.pos(), 4); + assert!(reader.is_eof()); + + reader.set_pos(5); + assert_eq!(reader.pos(), 5); + assert!(reader.is_eof()); + } + + #[test] + fn test_proto_input_error_display() { + assert_eq!( + format!("{}", ProtoInputError::UnexpectedEof), + "Unexpected EOF while parsing protobuf stream" + ); + assert_eq!( + format!("{}", ProtoInputError::VarintOverflow), + "Varint overflow (greater than 64 bits)" + ); + assert_eq!( + format!("{}", ProtoInputError::UnsupportedWireType(3)), + "Unsupported protobuf wire type: 3" + ); + } + + #[test] + fn test_read_record_varint() { + // Field 1, WireType 0 (Varint), Value 150 + // tag = (1 << 3) | 0 = 8 (0x08) + // value = 150 -> 0x96, 0x01 + let bytes = &[0x08, 0x96, 0x01]; + let mut reader = ProtoReader::new(bytes); + let (field, record) = reader.read_record().unwrap(); + assert_eq!(field, 1); + assert_eq!(record, Record::Varint(150)); + assert!(reader.is_eof()); + } + + #[test] + fn test_read_record_fixed64() { + // Field 2, WireType 1 (Fixed64), Value [1, 2, 3, 4, 5, 6, 7, 8] + // tag = (2 << 3) | 1 = 17 (0x11) + let bytes = &[0x11, 1, 2, 3, 4, 5, 6, 7, 8]; + let mut reader = ProtoReader::new(bytes); + let (field, record) = reader.read_record().unwrap(); + assert_eq!(field, 2); + assert_eq!(record, Record::Fixed64(0x0807060504030201)); + assert!(reader.is_eof()); + } + + #[test] + fn test_read_record_length_delimited() { + // Field 3, WireType 2 (LengthDelimited), Value "hello" (5 bytes) + // tag = (3 << 3) | 2 = 26 (0x1A) + // length = 5 + let bytes = &[0x1A, 0x05, b'h', b'e', b'l', b'l', b'o']; + let mut reader = ProtoReader::new(bytes); + let (field, record) = reader.read_record().unwrap(); + assert_eq!(field, 3); + assert_eq!(record, Record::LengthDelimited(b"hello")); + assert!(reader.is_eof()); + } + + #[test] + fn test_read_record_fixed32() { + // Field 4, WireType 5 (Fixed32), Value [9, 10, 11, 12] + // tag = (4 << 3) | 5 = 37 (0x25) + let bytes = &[0x25, 9, 10, 11, 12]; + let mut reader = ProtoReader::new(bytes); + let (field, record) = reader.read_record().unwrap(); + assert_eq!(field, 4); + assert_eq!(record, Record::Fixed32(0x0C0B0A09)); + assert!(reader.is_eof()); + } + + #[test] + fn test_read_record_errors() { + // Unexpected EOF on tag (empty) + let mut reader = ProtoReader::new(&[]); + assert_eq!( + reader.read_record().unwrap_err(), + ProtoInputError::UnexpectedEof + ); + + // Unexpected EOF on tag (partial varint) + let mut reader = ProtoReader::new(&[0x80]); + assert_eq!( + reader.read_record().unwrap_err(), + ProtoInputError::UnexpectedEof + ); + + // Varint overflow on tag + let mut reader = + ProtoReader::new(&[0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x02]); + assert_eq!( + reader.read_record().unwrap_err(), + ProtoInputError::VarintOverflow + ); + + // Unsupported wire type (Type 3) + // tag = (1 << 3) | 3 = 11 (0x0B) + let mut reader = ProtoReader::new(&[0x0B]); + assert_eq!( + reader.read_record().unwrap_err(), + ProtoInputError::UnsupportedWireType(3) + ); + + // Unexpected EOF on Varint value + // tag = 8 (field 1, varint), value starts but EOFs + let mut reader = ProtoReader::new(&[0x08, 0x80]); + assert_eq!( + reader.read_record().unwrap_err(), + ProtoInputError::UnexpectedEof + ); + + // Unexpected EOF on Fixed64 value + // tag = 17 (field 2, fixed64), but only 7 bytes follow + let mut reader = ProtoReader::new(&[0x11, 1, 2, 3, 4, 5, 6, 7]); + assert_eq!( + reader.read_record().unwrap_err(), + ProtoInputError::UnexpectedEof + ); + + // Unexpected EOF on LengthDelimited length (partial varint) + // tag = 26 (field 3, length delimited) + let mut reader = ProtoReader::new(&[0x1A, 0x80]); + assert_eq!( + reader.read_record().unwrap_err(), + ProtoInputError::UnexpectedEof + ); + + // Unexpected EOF on LengthDelimited value + // tag = 26, length = 5, but only 4 bytes follow + let mut reader = ProtoReader::new(&[0x1A, 0x05, b'h', b'e', b'l', b'l']); + assert_eq!( + reader.read_record().unwrap_err(), + ProtoInputError::UnexpectedEof + ); + + // Unexpected EOF on Fixed32 value + // tag = 37 (field 4, fixed32), but only 3 bytes follow + let mut reader = ProtoReader::new(&[0x25, 9, 10, 11]); + assert_eq!( + reader.read_record().unwrap_err(), + ProtoInputError::UnexpectedEof + ); + } + + #[test] + fn test_find_field() { + // Stream: + // Field 1: Varint 150 -> [0x08, 0x96, 0x01] + // Field 2: LengthDelimited "hello" -> [0x12, 0x05, b'h', b'e', b'l', b'l', b'o'] + // Field 3: Fixed32 [9, 10, 11, 12] -> [0x1D, 9, 10, 11, 12] (tag: (3<<3)|5 = 29 = 0x1D) + let bytes = &[ + 0x08, 0x96, 0x01, 0x12, 0x05, b'h', b'e', b'l', b'l', b'o', 0x1D, 9, 10, 11, 12, + ]; + + let reader = ProtoReader::new(bytes); + + // Find existing fields + assert_eq!(reader.find_field(1), Some(Record::Varint(150))); + assert_eq!( + reader.find_field(2), + Some(Record::LengthDelimited(b"hello")) + ); + assert_eq!(reader.find_field(3), Some(Record::Fixed32(0x0C0B0A09))); + + // Find non-existent field + assert_eq!(reader.find_field(4), None); + + // Find from a advanced position + let mut mut_reader = ProtoReader::new(bytes); + mut_reader.read_record().unwrap(); // consumes Field 1 + assert_eq!(mut_reader.pos(), 3); + + // find_field on mut_reader should start scanning from pos=3 + assert_eq!(mut_reader.find_field(1), None); // Field 1 is in the past + assert_eq!( + mut_reader.find_field(2), + Some(Record::LengthDelimited(b"hello")) + ); + assert_eq!(mut_reader.find_field(3), Some(Record::Fixed32(0x0C0B0A09))); + } + + #[test] + fn test_find_field_with_corruption() { + // Stream: + // Field 1: Varint 150 -> [0x08, 0x96, 0x01] + // Corrupted part: invalid tag or unexpected EOF + let bytes_corrupted_after = &[ + 0x08, 0x96, 0x01, // Field 1 + 0x12, 0x05, b'h', // Corrupted Field 2 (truncated length delimited) + ]; + + let reader = ProtoReader::new(bytes_corrupted_after); + // Since the stream is corrupted, we cannot find any fields because "last one wins" + // requires scanning the entire stream, which will encounter the corruption. + assert_eq!(reader.find_field(1), None); + // We cannot find Field 2 because it's corrupted + assert_eq!(reader.find_field(2), None); + // We cannot find Field 3 (which is not present, and scanning hits corruption) + assert_eq!(reader.find_field(3), None); + } + + #[test] + fn test_find_field_duplicate() { + // Stream: + // Field 1: Varint 150 -> [0x08, 0x96, 0x01] + // Field 1: Varint 200 -> [0x08, 0xC8, 0x01] + let bytes = &[ + 0x08, 0x96, 0x01, // Field 1 (first occurrence) + 0x08, 0xC8, 0x01, // Field 1 (second occurrence) + ]; + let reader = ProtoReader::new(bytes); + // "Last one wins" for singular fields: + assert_eq!(reader.find_field(1), Some(Record::Varint(200))); + } + + #[test] + fn test_record_decode_helpers() { + // Varint tests + let rec = Record::Varint(150); + assert_eq!(rec.decode_u64(FieldType::Uint64).unwrap(), 150); + assert_eq!(rec.decode_u64(FieldType::Int64).unwrap(), 150); + + // Sint32 / Sint64 zig-zag decoding (3 decodes to -2) + let rec = Record::Varint(3); + assert_eq!(rec.decode_u64(FieldType::Sint64).unwrap(), u64::MAX - 1); + assert_eq!(rec.decode_u64(FieldType::Sint32).unwrap(), u64::MAX - 1); + + // Fixed64 tests + let rec = Record::Fixed64(1); + assert_eq!(rec.decode_u64(FieldType::Fixed64).unwrap(), 1); + assert_eq!(rec.decode_u64(FieldType::Sfixed64).unwrap(), 1); + + // Sfixed64 negative tests + let rec = Record::Fixed64(u64::MAX); + assert_eq!(rec.decode_u64(FieldType::Sfixed64).unwrap(), u64::MAX); + + // Fixed32 tests + let rec = Record::Fixed32(1); + assert_eq!(rec.decode_u64(FieldType::Fixed32).unwrap(), 1); + + // Sfixed32 negative tests (u32::MAX is -1 as i32, which sign-extends to u64::MAX) + let rec = Record::Fixed32(u32::MAX); + assert_eq!(rec.decode_u64(FieldType::Sfixed32).unwrap(), u64::MAX); + + // Unsupported wire type mismatches + let rec = Record::Varint(100); + assert_eq!( + rec.decode_u64(FieldType::Fixed32).unwrap_err(), + ProtoInputError::UnsupportedWireType(0) + ); + + let rec = Record::Fixed32(0); + assert_eq!( + rec.decode_u64(FieldType::Uint32).unwrap_err(), + ProtoInputError::UnsupportedWireType(5) + ); + } +} diff --git a/shared/proto/reflect.rs b/shared/proto/reflect.rs new file mode 100644 index 00000000..59a46f44 --- /dev/null +++ b/shared/proto/reflect.rs @@ -0,0 +1,82 @@ +// Copyright 2026 Google LLC +// +// 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. + +//! Reflection and metadata helpers for Protobuf types. + +use descriptor_rust_proto::field_descriptor_proto::Type; + +/// Metadata about a primitive Protobuf field type. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct PrimitiveInfo { + /// The size in bytes of the primitive type. + pub size: usize, + /// Whether the primitive type is signed. + pub is_signed: bool, + /// Whether the primitive type is a floating-point type. + pub is_float: bool, +} + +/// Extension trait for Protobuf [`Type`]. +pub trait TypeExt { + /// Returns the primitive metadata for this type, or `None` if the type is not a packable primitive. + fn primitive_info(&self) -> Option; +} + +impl TypeExt for Type { + fn primitive_info(&self) -> Option { + match *self { + Type::Double => Some(PrimitiveInfo { + size: 8, + is_signed: true, + is_float: true, + }), + Type::Float => Some(PrimitiveInfo { + size: 4, + is_signed: true, + is_float: true, + }), + Type::Int64 | Type::Sint64 | Type::Sfixed64 => Some(PrimitiveInfo { + size: 8, + is_signed: true, + is_float: false, + }), + Type::Uint64 | Type::Fixed64 => Some(PrimitiveInfo { + size: 8, + is_signed: false, + is_float: false, + }), + Type::Int32 | Type::Sint32 | Type::Sfixed32 => Some(PrimitiveInfo { + size: 4, + is_signed: true, + is_float: false, + }), + Type::Uint32 | Type::Fixed32 => Some(PrimitiveInfo { + size: 4, + is_signed: false, + is_float: false, + }), + Type::Bool => Some(PrimitiveInfo { + size: 1, + is_signed: false, + is_float: false, + }), + Type::Enum => Some(PrimitiveInfo { + size: 4, + is_signed: true, + is_float: false, + }), + _ => None, + } + } +} diff --git a/shared/tlv/BUILD.bazel b/shared/tlv/BUILD.bazel new file mode 100644 index 00000000..2e690a6a --- /dev/null +++ b/shared/tlv/BUILD.bazel @@ -0,0 +1,146 @@ +# Copyright 2026 Google LLC +# +# 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. + +load("@protobuf//bazel:proto_library.bzl", "proto_library") +load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_library", "rust_test") + +proto_library( + name = "options_proto", + srcs = ["options.proto"], + visibility = ["//visibility:public"], + deps = ["@protobuf//:descriptor_proto"], +) + +rust_library( + name = "tlv", + srcs = [ + "builder.rs", + "lib.rs", + ], + crate_name = "openprot_tlv", + edition = "2024", + proc_macro_deps = [ + "//third_party:bitfield_struct", + ], + visibility = ["//visibility:public"], + deps = [ + "//third_party:zerocopy", + ], +) + +rust_library( + name = "proto_conv", + srcs = ["proto_conv.rs"], + crate_name = "openprot_tlv_proto_conv", + edition = "2024", + visibility = ["//visibility:public"], + deps = [ + ":proto_meta", + ":tlv", + "//shared/proto:descriptor_rust_proto", + "//shared/proto:reader", + "//shared/proto:reflect", + "//third_party:zerocopy", + "@protobuf//rust:protobuf", + ], +) + +rust_library( + name = "proto_meta", + srcs = ["proto_meta.rs"], + compile_data = ["//shared/tlv/test:bad_proto"], + crate_name = "openprot_tlv_proto_meta", + data = ["//shared/tlv/test:bad_proto"], + edition = "2024", + rustc_env = { + "BAD_PROTO_DESCRIPTOR_SET": "$(execpath //shared/tlv/test:bad_proto)", + }, + visibility = ["//visibility:public"], + deps = [ + "//shared/proto:descriptor_rust_proto", + "//shared/proto:reader", + "//shared/proto:reflect", + "@protobuf//rust:protobuf", + ], +) + +rust_test( + name = "proto_conv_test", + crate = ":proto_conv", + edition = "2024", + deps = [ + "//shared/tlv/test:edge_cases_rust_proto", + "//shared/tlv/test:edge_cases_tlv", + "//shared/tlv/test:soc_rust_proto", + "//shared/tlv/test:soc_tlv", + "//third_party:bstr", + ], +) + +rust_test( + name = "proto_meta_test", + crate = ":proto_meta", + edition = "2024", + deps = [ + "//shared/tlv/test:bad_rust_proto", + "//shared/tlv/test:edge_cases_rust_proto", + ], +) + +rust_test( + name = "tlv_test", + crate = ":tlv", + edition = "2024", +) + +rust_binary( + name = "codegen", + srcs = ["codegen.rs"], + edition = "2024", + visibility = ["//visibility:public"], + deps = [ + ":proto_meta", + "//shared/proto:descriptor_rust_proto", + "//shared/proto:reflect", + "//third_party:anyhow", + "//third_party:argh", + "//third_party:proc_macro2", + "//third_party:quote", + "@protobuf//rust:protobuf", + ], +) + +rust_test( + name = "codegen_test", + srcs = [ + "codegen_test.rs", + ], + edition = "2024", + deps = [ + ":tlv", + "//shared/tlv/test:edge_cases_tlv", + "//shared/tlv/test:import_base_tlv", + "//shared/tlv/test:import_layered_tlv", + "//shared/tlv/test:import_pure_tlv", + "//shared/tlv/test:import_simple_tlv", + "//shared/tlv/test:soc_tlv", + "//third_party:bstr", + ], +) + +rust_test( + name = "codegen_unit_test", + crate = ":codegen", + edition = "2024", +) diff --git a/shared/tlv/PROTOBUF.md b/shared/tlv/PROTOBUF.md new file mode 100644 index 00000000..42d92628 --- /dev/null +++ b/shared/tlv/PROTOBUF.md @@ -0,0 +1,364 @@ +# Protobuf-to-TLV Integration & Code Generation + +This document describes how the TLV framework integrates with Protocol Buffers +(Protobuf) to enable schema authoring, code generation, and host-to-embedded +serialization. + +--- + +## Architectural Role + +While the embedded system uses highly optimized, zero-copy Rust structures at +runtime, authoring schemas directly in Rust can be tedious and error-prone. +Furthermore, host-side tools (written in Python, Go, C++, etc.) need a unified, +standard way to construct and serialize these messages. + +The TLV framework solves this by utilizing Protobuf as a schema definition +language. The schema compiles into static Rust structures for the embedded +system, while host-side tools serialize standard Protobuf messages into the +aligned binary TLV format using reflection: + +```text + [ Protobuf Schema (.proto) ] + / \ + / \ + v \ (descriptor set) + [ ":codegen" ] v + | [ ":proto_conv" ] <--- [ Protobuf Bytes ] + (generates) | | + v | + [ Generated Rust ] | + | | + (compiled) | | (proto_to_tlv) + v v + [ Zero-Copy Structs ] [ Packed TLV Bytes ] + \ / + \ / (communications) + v v + +----------------------+ + | Embedded Side | + | [Zero-Copy Overlay] | + +----------------------+ +``` + +--- + +## Custom Options (`options.proto`) + +The framework extends standard Protobuf descriptors with custom options defined +in [options.proto](options.proto). These options direct the code generator and the runtime serialization converter. + +### 1. Message Options + +* **`tlv_fourcc` (string):** Assigns a 4-character ASCII tag (FourCC) to the + message. Any message with this option is compiled into a top-level TLV record + type implementing `TlvObject`. +* **`bit_packed` (string):** Specifies the backing primitive integer type + (`"u8"`, `"u16"`, or `"u32"`) for a bitfield struct. When present, the + message is generated as a packed bitfield rather than a standard TLV. + +### 2. Field Options + +* **`offset` (uint32):** Specifies the byte offset of the field within the fixed + struct of the TLV record. + * Fields with an `offset` are compiled **inline** into the fixed struct. + * Offsets must match the natural alignment of the field's backing Rust type + (e.g., a `u32` field must reside at an offset divisible by 4). +* **`type` (string):** Overrides the generated Rust type. This is typically + used to pack fields into smaller primitive integers (e.g., packing a Protobuf + `uint32` into a Rust `u8` or `u16`) or to represent fixed-size arrays (e.g., + `"[u8; 16]"` for strings or `"[u32; 4]"` for UUIDs). +* **`bit_range` (string):** Specifies the bit range of a field in a bit-packed + message using Verilog notation (e.g., `"0"` for a single bit, or `"7:3"` for + a multi-bit range). +* **`extra_data` (bool):** Indicates that a `string` or `bytes` field is + stored as the unpadded variable-length extra data following the fixed payload + fields of the TLV record, rather than an inline fixed struct field or dynamic + sub-TLV. Must be accompanied by an explicit `type` option (e.g. + `[(openprot.tlv.type) = "[u8]"]`). Fields marked with `extra_data` cannot specify + an `offset` option. + +### 3. Enum Options + +* **`raw_type` (string):** Specifies the backing primitive integer type for an + enum (e.g., `"u8"`, `"u16"`, `"u32"`, `"i32"`). + * Enums with this option are generated as type-safe **open enums** in Rust, + which wrap the raw integer to prevent undefined behavior when casting + arbitrary/invalid bytes from the wire. + +--- + +## Layout Mapping Rules + +A Protobuf message annotated for TLV is split into a **Fixed Struct** and a +**Variable-Length Extension**: + +### The Fixed Struct (Inline Fields) +* Every field in the Protobuf message annotated with a `[(openprot.tlv.offset)]` + is packed directly into the fixed struct at that byte offset. +* The total size of the fixed struct is padded to a multiple of 4 bytes. +* Nested messages can be embedded inline in the fixed struct if their type is + a bitfield message (annotated with `bit_packed`) or a plain struct without a + `tlv_fourcc`. + +### The Variable-Length Extension + +The payload words following the fixed struct carry the variable-length extension, +which can be either **Extra Data** or **Nested Sub-TLVs** (mutually exclusive): + +#### 1. Extra Data (`extra_data`) +* A `string` or `bytes` field annotated with `[(openprot.tlv.extra_data) = true]` + is stored directly as the unpadded variable-length extra data trailing the fixed struct. +* Must specify an explicit slice `type` option (at the moment, only `[(openprot.tlv.type) = "[u8]"]` is supported). +* A message may have at most **one** field marked with `extra_data`. +* A message with `extra_data` cannot also contain dynamic sub-TLVs. + +#### 2. Nested Sub-TLVs (Dynamic TLVs) +* Any message field **without** a `[(openprot.tlv.offset)]` is serialized as a + **nested TLV record** inside the parent TLV's extension. +* This applies to: + * `repeated` fields of other TLV messages (serialized as a sequence of nested + records). + * Singular fields of other TLV messages (serialized as a single nested + record). + * `oneof` unions of TLV messages (serialized as a single nested record representing + the active variant). + +--- + +## Code Generation + +The code generator (`tlv_codegen`) processes compiled Protobuf descriptor sets +(`FileDescriptorSet`) and outputs a single Rust module containing: +1. Standard zero-copy structs implementing `TlvObject` (for messages with + `tlv_fourcc`). +2. Plain zero-copy helper structs (for nested inline messages). +3. Bitfield structs using `bitfield_struct::bitfield` (for messages with + `bit_packed`). +4. Open enum wrappers (for enums with `raw_type`). +5. Accessor traits (`Ext`) implemented on `TlvItem<'a, >` providing: + * Typed iterators and getters for nested sub-TLVs (e.g., `components()`). + * Zero-copy slice or string accessors for `extra_data` fields (`&'a [u8]`, `&'a [T]`, or `&'a bstr::BStr`). + +All generated structs automatically derive the necessary `zerocopy` traits +(`FromBytes`, `IntoBytes`, `Immutable`, `KnownLayout`). + +--- + +## Bazel Integration (`tlv_codegen` macro) + +The TLV framework provides a custom Bazel macro, `tlv_codegen` (defined in +[rules.bzl](rules.bzl)), to automate the code generation and compilation +pipeline. It compiles a `proto_library` directly into a first-class Rust +library (`rust_library`). + +### Usage + +To generate a Rust library from a `proto_library`, load the macro in your +`BUILD.bazel` file: + +```bazel +load("//shared/tlv:rules.bzl", "tlv_codegen") + +proto_library( + name = "soc_proto", + srcs = ["soc.proto"], + deps = ["//shared/tlv:options_proto"], +) + +tlv_codegen( + name = "soc_tlv", + proto = ":soc_proto", + src = "soc_tlv.rs", # Recommended: checked-in source +) +``` + +### Golden File & IDE Integration Pattern + +The `tlv_codegen` macro supports a highly developer-friendly "golden file" +pattern when the optional `src` attribute is specified: + +1. **IDE Friendliness:** By specifying a checked-in file (`src = "soc_tlv.rs"`), + the compiler and IDE tools (like `rust-analyzer`) can index the generated + code directly inside the source tree, providing full autocomplete and + go-to-definition support. +2. **Build-Time Verification:** Bazel automatically registers a verification + action that compares the checked-in `src` file against a freshly generated + one at build-time. If the checked-in file is stale (e.g., because the `.proto` + file was modified), the build fails with a clear error. +3. **Automated Updates:** If the build fails due to a stale checked-in file, a + companion binary target named `_update` is automatically defined. + Executing this target will automatically overwrite the checked-in file with the + latest generated code: + ```bash + bazel run //path/to/package:soc_tlv_update + ``` + +--- + +## Serialization Pipeline (`proto_conv`) + +The `proto_conv` module provides the host-side converter that packs standard +serialized Protobuf bytes into the aligned binary TLV format. + +### Key APIs + +```rust +// Encodes raw serialized Protobuf bytes into a packed, 32-bit aligned TLV buffer. +pub fn proto_to_tlv( + desc_set: &FileDescriptorSet, + message_name: &str, + proto_bytes: &[u8], +) -> Result, ProtoConvError>; + +// Helper that serializes a Rust Protobuf struct and encodes it to TLV. +pub fn message_to_tlv( + info: &DescriptorInfo, + message: &M, +) -> Result, ProtoConvError>; +``` + +### Validation Performed During Serialization +The converter performs rigorous checks to ensure the Protobuf data fits the +static constraints of the target Rust structures: +* **Range Checks:** Verifies that integer values fit within their overridden + bit-widths (e.g., a value in a `bit_range` field or a field overridden with `u8`). +* **Length Checks:** Verifies that string or byte fields overridden with fixed-size + arrays (like `[u8; 16]`) do not exceed their allocated size. +* **Alignment Checks:** Verifies that all field offsets are correctly aligned. + +--- + +## Comprehensive Example + +### 1. Protobuf Definition +```protobuf +syntax = "proto3"; +import "shared/tlv/options.proto"; + +enum SystemState { + option (openprot.tlv.raw_type) = "u8"; + SYSTEM_IDLE = 0; + SYSTEM_ACTIVE = 1; +} + +message Features { + option (openprot.tlv.bit_packed) = "u8"; + bool enable_dma = 1 [(openprot.tlv.bit_range) = "0"]; + uint32 speed_grade = 2 [(openprot.tlv.bit_range) = "3:1"]; +} + +message SubComponent { + option (openprot.tlv.tlv_fourcc) = "SUB_"; + uint32 id = 1 [(openprot.tlv.offset) = 0]; +} + +message Device { + option (openprot.tlv.tlv_fourcc) = "DEV_"; + + uint32 device_id = 1 [(openprot.tlv.offset) = 0]; + SystemState state = 2 [(openprot.tlv.offset) = 4]; + Features features = 3 [(openprot.tlv.offset) = 5]; + + // Nested TLV (no offset) + repeated SubComponent components = 4; +} +``` + +### 2. Generated Rust Layout +The generator compiles the above definition into the following Rust code: + +```rust +#[derive(Clone, Copy, PartialEq, Eq, Hash, Default, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(transparent)] +pub struct SystemState(pub u8); +impl SystemState { + pub const IDLE: Self = Self(0); + pub const ACTIVE: Self = Self(1); +} +impl ::core::fmt::Debug for SystemState { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + match *self { + Self::IDLE => write!(f, "IDLE"), + Self::ACTIVE => write!(f, "ACTIVE"), + other => write!(f, "{}({})", stringify!(SystemState), other.0), + } + } +} + +#[bitfield(u8)] +#[derive(PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +pub struct Features { + #[bits(1)] + pub enable_dma: bool, + #[bits(3)] + pub speed_grade: u8, + #[bits(4)] + __: (), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct SubComponent { + pub id: u32, +} +const _: () = { + assert!(::core::mem::offset_of!(SubComponent, id) == 0usize); + assert!(::core::mem::align_of::() <= 4); + assert!(::core::mem::size_of::() == 4usize); +}; +impl ::openprot_tlv::TlvObject for SubComponent { + type Extension = [u8]; + const TAG: u32 = u32::from_le_bytes(*b"SUB_"); + fn make_ext<'a>(header: &::openprot_tlv::TlvHeader, extra: &'a [u32]) -> &'a Self::Extension { + ::openprot_tlv::unpad(header, extra) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct Device { + pub device_id: u32, + pub state: SystemState, + pub features: Features, + pub __pad0: [u8; 2usize], +} +const _: () = { + assert!(::core::mem::offset_of!(Device, device_id) == 0usize); + assert!(::core::mem::offset_of!(Device, state) == 4usize); + assert!(::core::mem::offset_of!(Device, features) == 5usize); + assert!(::core::mem::align_of::() <= 4); + assert!(::core::mem::size_of::() == 8usize); +}; +impl ::openprot_tlv::TlvObject for Device { + type Extension = ::openprot_tlv::TlvData; + const TAG: u32 = u32::from_le_bytes(*b"DEV_"); + fn make_ext<'a>(_header: &::openprot_tlv::TlvHeader, extra: &'a [u32]) -> &'a Self::Extension { + ::openprot_tlv::TlvData::overlay(extra) + } +} +pub trait DeviceExt<'a> { + fn components(&self) -> ::openprot_tlv::TlvIterator<'a, SubComponent>; +} +impl<'a> DeviceExt<'a> for ::openprot_tlv::TlvItem<'a, Device> { + fn components(&self) -> ::openprot_tlv::TlvIterator<'a, SubComponent> { + ::openprot_tlv::TlvQuery::ext(self).iter::() + } +} +``` + +## Real-World Examples in the Repository + +For complete, working examples of Protobuf definitions, generated Rust code, +and TLV byte generation, refer to the files checked into the repository: + +* **System-on-Chip (SoC) Schema:** + * Protobuf definition: [test/soc.proto](test/soc.proto) + * Generated Rust code: [test/soc_tlv.rs](test/soc_tlv.rs) + * Bazel target: `//shared/tlv/test:soc_tlv` +* **TLV Byte Generation & Serialization:** + * Serialization tests: [proto_conv.rs](proto_conv.rs) (see the test function + `test_proto_to_tlv_system_hierarchy`) + * This test demonstrates how to construct the Protobuf message structures + and invoke `message_to_tlv` to generate the aligned, packed binary TLV + words. diff --git a/shared/tlv/README.md b/shared/tlv/README.md new file mode 100644 index 00000000..19cea86f --- /dev/null +++ b/shared/tlv/README.md @@ -0,0 +1,243 @@ +# Zero-Copy Type-Length-Value (TLV) Wire Format + +This document defines the binary wire format of the Type-Length-Value (TLV) +serialization framework used in OpenPRoT. + +--- + +## Design Philosophy + +The TLV framework is designed for high-performance, resource-constrained +environments, in particular embedded systems with limited memory and storage. +Its core design goals are: +1. **Zero-Copy Parsing:** Structured views are overlaid directly onto raw + byte/word buffers using the `zerocopy` crate, eliminating parsing + allocations and copies. +2. **Word Alignment:** All headers, payloads, and nested elements are strictly + aligned to **32-bit (4-byte) word boundaries** to match the native alignment + of 32-bit CPUs (e.g., RV32IMC, ARMv7-M) and maximize memory access efficiency. +3. **Robust Schema Evolution:** The wire format natively supports forward and + backward compatibility via explicit payload length fields and zero-extended + casting. + +--- + +## Protobuf Schema & Code Generation + +Rather than authoring TLV structures manually in Rust, schemas can be defined +using standard Protocol Buffers (Protobuf) annotated with custom layout options +(such as FourCC tags, offsets, and bit ranges). + +A code generator (`tlv_codegen`) automatically compiles these `.proto` definitions +into the corresponding zero-copy Rust structures, and a serialization pipeline +(`proto_conv`) is provided to pack host-side Protobuf messages into the aligned +binary wire format. + +For a comprehensive guide on schema definition, custom options, and the +serialization pipeline, see [Protobuf Schema & Code Generation](PROTOBUF.md). + +--- + +## Wire Layout Overview + +Every TLV object consists of a **fixed 8-byte (2-word) header**, followed by a +**variable-length payload** whose size is a multiple of 4 bytes. + +```text + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| Tag (u32) | Word 0 (Header) ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| Word Length (u16) | Reserved (u8) | Flags (u8) | Word 1 (Header) ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| | Word 2 (Payload: Fixed Struct) ++ Fixed-Size Struct (U) + +| | Word N ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| | Word N+1 (Payload: Extension) ++ Variable-Length Extension + +| | Word N+M ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +``` + +--- + +## Header Fields + +The header is exactly 8 bytes long and contains the following fields: + +### 1. Tag (`u32`) - 4 Bytes (Offset 0) +* Identifies the type of the TLV object, typically as a 4-character ASCII FourCC. +* **Byte Order:** The ASCII characters are laid out sequentially from offset 0 + to 3 (e.g., `*b"FOO_"` is stored on the wire as the bytes `46 4F 4F 5F`). +* **Integer Value:** Interpreted in Rust as a little-endian `u32` (via + `u32::from_le_bytes`). For example, `*b"FOO_"` yields the integer + `0x5F4F4F46`. + +### 2. Word Length (`u16`) - 2 Bytes (Offset 4) +* The length of the payload following this header, measured in **32-bit words** + (not bytes). +* A value of `0` indicates that no payload follows the header. + +### 3. Reserved (`u8`) - 1 Byte (Offset 6) +* Reserved for future protocol additions. +* **Must be set to `0`** by generators and ignored by readers. + +### 4. Flags (`u8`) - 1 Byte (Offset 7) +* Packed bitfield configuration. +* **Padding Bytes (Bits 0-1):** A 2-bit value (0 to 3) indicating the number of + trailing padding bytes in the last word of the payload. This is used when the + variable-length extension is a slice of bytes or half-words (e.g., `[u8]` or + `[u16]`) and the exact byte length is meaningful. +* **Reserved (Bits 2-7):** Must be set to `0`. + +--- + +## Payload Structure + +The payload starts immediately after the 8-byte header and is divided into two +logical sections: + +1. **Fixed-Size Struct (`U`):** A fixed-size struct containing the static + fields of the TLV object. Its size in bytes must be a multiple of 4. +2. **Variable-Length Extension:** Any remaining words in the payload after the + fixed struct. + +### Length Calculations + +The payload contains the **Fixed Struct** followed by the **Extension**. Their +sizes are derived from the header's `Word Length` and `Padding Bytes` as follows: + +```text +payload_len = header.word_len * 4 +struct_len = sizeof(U).next_multiple_of(4) +extension_len = payload_len - struct_len - header.flags.padding_bytes +``` + +## Extension Types + +The variable-length extension can represent two different types of data: + +### 1. Extra Data +When a TLV record is defined with extra data (annotated with `(openprot.tlv.extra_data) = true` +in Protobuf, or `type Extension = [u8]` in Rust), the payload following the fixed struct +is interpreted as raw bytes or a typed slice. +* The generator fills the extra data bytes, pads the final word to 4 bytes with + zeros, and sets `Padding Bytes` in the header flags to the number of padding + bytes added (0-3). +* Readers retrieve the unpadded slice via `TlvItem::ext()` or generated field accessors. + +### 2. Nested TLV Container (`TlvData`) +If the TLV object is defined with a `TlvData` extension, the extension is +interpreted as a sequence of zero or more nested TLV objects. +* Because nested TLVs are themselves structured and word-aligned, **no + byte-level padding is used** at the container level. The `Padding Bytes` + field in the parent header flags **must be set to `0`**. +* The nested TLVs are laid out sequentially within the parent's extension + words, allowing recursive, zero-copy parsing. + +--- + +## Compatibility & Schema Evolution + +The TLV framework supports robust schema evolution. Here are the mechanisms and +rules for modifying structures safely: + +### 1. Adding New TLV Records +* New TLV record types (with unique tags) can be freely added to a stream. +* Existing readers using `TlvIterator` will automatically skip unrecognized + tags by reading the `word_len` field in the header to jump to the next record, + ensuring full backward compatibility. + +### 2. Modifying Existing Structs (Field Additions) +New fields can be appended to the end of an existing fixed struct `U`, subject to +the following strict rules and compatibility behaviors: + +#### Rules & Constraints: +* **No Variable-Length Extensions:** Fields can **only** be added to `U` if the + record does not carry variable-length data (such as nested TLV sub-records or + an `extra_data` payload). + * *Why:* Appending fields to `U` increases `sizeof(U)`, shifting the start + offset of the extension. A newer reader will look for the extension at the + new offset, failing to parse or misinterpreting data generated by older writers. +* **Future Constraint:** Adding fields to `U` **prevents** subsequent introduction + of nested TLVs or variable-length `extra_data`, as any further field additions + would break the start offset of the variable-length data across versions. + +#### Backward Compatibility (Underflow): +If a reader compiled with a newer, larger version of struct `U` receives a +payload generated by an older, smaller version, the payload length will be +smaller than `sizeof(U)`: +* The `cast_zero_extended` method allocates `U` on the stack, initializes it to + all-zeros (`U::new_zeroed()`), and copies the available payload bytes. +* The new fields at the end of `U` naturally remain zeroed, allowing safe, + backward-compatible default behavior. + +#### Forward Compatibility (Overflow): +If an older reader receives a newer, larger payload: +* **For regular casting (`cast`):** The excess words are cleanly separated into + the `extra` field, allowing the reader to access the base struct while + ignoring or forwarding the new fields. +* **For zero-extended casting (`cast_zero_extended`):** The method copies only + the bytes that fit into the target struct `U` and discards the excess bytes, + preventing overflow or parser failure. + +--- + +## Wire Examples + +### Example 1: Simple Struct with Raw Byte Extension +Consider a TLV object defined as: +```rust +tlv_struct! { Foo, *b"FOO_", [u8], + pub struct Foo { + pub value: u32, + } +} +``` +If we construct a `Foo` with `value = 0x12345678` and a raw byte extension of +`[0xAA, 0xBB, 0xCC]` (3 bytes): +* The fixed struct size is 4 bytes (1 word). +* The extension is 3 bytes, requiring 1 word (4 bytes) with 1 byte of padding. +* Total payload is 2 words (`word_len = 2`). +* `Padding Bytes` is `1`. + +#### Byte Layout: +```hexdump +Address/Word Offset Byte Values Description +-------------------------------------------------------------------------------- +Word 0 00..03 46 4F 4F 5F Tag: 'FOO_' (0x5F4F4F46) +Word 1 04..05 02 00 Word Length: 2 (payload is 8 bytes) + 06 00 Reserved: 0 + 07 01 Flags: padding_bytes = 1 (bits 0-1) +Word 2 08..11 78 56 34 12 Foo.value: 0x12345678 (little-endian) +Word 3 12..14 AA BB CC Extension data: [0xAA, 0xBB, 0xCC] + 15 00 Padding byte (0x00) +``` + +### Example 2: Nested TLV Container +Consider a parent TLV object `Group` containing nested TLVs: +* Parent Tag: `'GRP_'` (`0x5F505247`) +* Parent Fixed Struct: 4 bytes (e.g., a single ID field `group_id = 0x99`). +* Extension: `TlvData` containing two nested child TLVs (each child is a `Foo` + with 0-word payload, tag `'FOO_'`, `word_len = 0`). + +#### Byte Layout: +```hexdump +Address/Word Offset Byte Values Description +-------------------------------------------------------------------------------- +Word 0 00..03 47 52 50 5F Parent Tag: 'GRP_' +Word 1 04..05 05 00 Parent Word Length: 5 (20 bytes payload) + 06 00 Parent Reserved: 0 + 07 00 Parent Flags: padding_bytes = 0 +Word 2 08..11 99 00 00 00 Parent.group_id: 0x99 +Word 3 12..15 46 4F 4F 5F Child 1 Tag: 'FOO_' +Word 4 16..17 00 00 Child 1 Word Length: 0 + 18 00 Child 1 Reserved: 0 + 19 00 Child 1 Flags: padding_bytes = 0 +Word 5 20..23 46 4F 4F 5F Child 2 Tag: 'FOO_' +Word 6 24..25 00 00 Child 2 Word Length: 0 + 26 00 Child 2 Reserved: 0 + 27 00 Child 2 Flags: padding_bytes = 0 +``` diff --git a/shared/tlv/builder.rs b/shared/tlv/builder.rs new file mode 100644 index 00000000..994a1011 --- /dev/null +++ b/shared/tlv/builder.rs @@ -0,0 +1,244 @@ +// Copyright 2026 Google LLC +// +// 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. + +use zerocopy::IntoBytes; +use zerocopy::transmute; +use zerocopy::transmute_mut; + +use crate::TlvContainer; +use crate::TlvError; +use crate::TlvFlags; +use crate::TlvHeader; +use crate::TlvItem; +use crate::TlvObject; + +/// A builder for creating TLV objects. +pub struct TlvBuilder<'a> { + header: &'a mut TlvHeader, + remaining: &'a mut [u32], +} + +impl<'a> TlvBuilder<'a> { + /// Creates a new [`TlvBuilder`] for the given buffer. + pub fn new(data: &'a mut [u32]) -> Result<(&'a mut TRoot, Self), TlvError> { + let (header_words, remaining) = data + .split_first_chunk_mut::<{ crate::word_size_of::() }>() + .ok_or(TlvError)?; + let header: &mut TlvHeader = transmute_mut!(header_words); + *header = TlvHeader { + tag: TRoot::TAG, + word_len: u16::try_from(crate::word_size_of::()).map_err(|_| TlvError)?, + reserved: 0, + flags: TlvFlags::new(), + }; + let (data_words, remaining) = remaining + .split_at_mut_checked(crate::word_size_of::()) + .ok_or(TlvError)?; + let data = TRoot::mut_from_prefix(data_words.as_mut_bytes()).unwrap().0; + Ok((data, Self { header, remaining })) + } + + /// Returns a reference to the header of the TLV object being built. + pub fn header(&self) -> &TlvHeader { + self.header + } + + /// Allocates space for a new fixed-size child TLV and returns a mutable reference to it. + /// + /// NOTE: On error, the builder is spoiled and finishing it yields an `InvalidBufferError`. + pub fn add(&mut self) -> Result<&'a mut T, TlvError> { + let remaining = core::mem::take(&mut self.remaining); + let (header_words, remaining) = remaining + .split_first_chunk_mut::<{ crate::word_size_of::() }>() + .ok_or(TlvError)?; + *header_words = transmute!(TlvHeader { + tag: T::TAG, + word_len: u16::try_from(crate::word_size_of::()).map_err(|_| TlvError)?, + reserved: 0, + flags: TlvFlags::new(), + }); + let (data_words, remaining) = remaining + .split_at_mut_checked(crate::word_size_of::()) + .ok_or(TlvError)?; + self.remaining = remaining; + + // Update current header length + let added_words = u16::try_from(crate::word_size_of::() + data_words.len()) + .map_err(|_| TlvError)?; + self.header.word_len = self + .header + .word_len + .checked_add(added_words) + .ok_or(TlvError)?; + + // Panic is impossible since data_words is guaranteed to be bigger than T + Ok(T::mut_from_prefix(data_words.as_mut_bytes()).unwrap().0) + } + + /// Allocates space for a new child TLV, and writes the supplied ext_bytes variable-length + /// extension. The returned mutable reference can be used to initialize the fixed-size fields of + /// the TLV. + /// + /// NOTE: On error, the builder is spoiled and finishing it yields an `InvalidBufferError`. + pub fn add_with_ext(&mut self, ext_bytes: &[u8]) -> Result<&'a mut T, TlvError> { + let remaining = core::mem::take(&mut self.remaining); + let struct_words = crate::word_size_of::(); + let ext_len = ext_bytes.len(); + let padded_ext_len = ext_len.checked_add(3).map(|n| n & !3).ok_or(TlvError)?; + let ext_words = padded_ext_len / 4; + let padding = u8::try_from(padded_ext_len - ext_len).unwrap(); + + let total_payload_words = struct_words + ext_words; + + let (header_words, remaining) = remaining + .split_first_chunk_mut::<{ crate::word_size_of::() }>() + .ok_or(TlvError)?; + + let header = TlvHeader { + tag: T::TAG, + word_len: u16::try_from(total_payload_words).map_err(|_| TlvError)?, + reserved: 0, + flags: TlvFlags::new().with_padding_bytes(padding), + }; + *header_words = transmute!(header); + + let (data_words, remaining) = remaining + .split_at_mut_checked(struct_words) + .ok_or(TlvError)?; + + let (ext_words_slice, remaining) = + remaining.split_at_mut_checked(ext_words).ok_or(TlvError)?; + + self.remaining = remaining; + + let ext_bytes_mut = ext_words_slice.as_mut_bytes(); + ext_bytes_mut[..ext_len].copy_from_slice(ext_bytes); + ext_bytes_mut[ext_len..].fill(0); + + let added_words = u16::try_from(crate::word_size_of::() + total_payload_words) + .map_err(|_| TlvError)?; + self.header.word_len = self + .header + .word_len + .checked_add(added_words) + .ok_or(TlvError)?; + + Ok(T::mut_from_prefix(data_words.as_mut_bytes()).unwrap().0) + } + + /// Allocates space for the fixed size portion of a child TLV, and returns a nested TlvBuilder + /// that can be used to add children to the child TLV. When all the children have been added, + /// call `finish_with_parent()` before adding more children to the parent. + /// + /// NOTE: Consider using `add_in()` instead, which is less error-prone. + pub fn add_with_children( + &mut self, + ) -> Result<(&'a mut T, TlvBuilder<'a>), TlvError> { + let remaining = core::mem::take(&mut self.remaining); + Self::new::(remaining) + } + + /// Finishes this child builder, restoring and updating the parent builder. + pub fn finish_with_parent(self, parent: &mut TlvBuilder<'a>) -> Result<(), TlvError> { + parent.remaining = self.remaining; + + let child_payload_len = usize::from(self.header.word_len); + let child_total_len_words = crate::word_size_of::() + child_payload_len; + let child_total_len_words_u16 = + u16::try_from(child_total_len_words).map_err(|_| TlvError)?; + parent.header.word_len = parent + .header + .word_len + .checked_add(child_total_len_words_u16) + .ok_or(TlvError)?; + Ok(()) + } + + /// A closure-based helper built on top of our linear primitives. + /// Automatically finishes the child builder when the closure completes. + pub fn add_in(&mut self, f: F) -> Result<&'a mut T, TlvError> + where + F: FnOnce(&mut T, &mut TlvBuilder<'a>) -> Result<(), TlvError>, + { + let (data, mut child_builder) = self.add_with_children::()?; + f(data, &mut child_builder)?; + child_builder.finish_with_parent(self)?; + Ok(data) + } + + /// Adds a copy of `item` (including its children) as a child of the builder. + /// + /// NOTE: On error, the builder is spoiled and finishing it yields an `InvalidBufferError`. + #[inline(always)] + pub fn add_copy(&mut self, item: &TlvItem<'_, T>) -> Result<(), TlvError> { + self.add_copy_container(item.container) + } + + /// Non-generic implementation of [`Self::add_copy`], to avoid monomorphization bloat. + fn add_copy_container(&mut self, container: &TlvContainer) -> Result<(), TlvError> { + let src_words = container.as_words(); + let remaining = core::mem::take(&mut self.remaining); + let (dest_words, remaining) = remaining + .split_at_mut_checked(src_words.len()) + .ok_or(TlvError)?; + dest_words.copy_from_slice(src_words); + self.remaining = remaining; + + // Update current header length + let added_words = u16::try_from(src_words.len()).map_err(|_| TlvError)?; + self.header.word_len = self + .header + .word_len + .checked_add(added_words) + .ok_or(TlvError)?; + Ok(()) + } + + /// Finish the builder, making the constructed bytes (or words) available as members of the + /// returned `TlvBuilderFinisher`. + pub fn finish(self) -> TlvBuilderFinisher { + TlvBuilderFinisher { + start_ptr: self.header as *const _ as *const u32, + end_ptr: self.remaining.as_mut_ptr(), + } + } +} + +#[derive(Debug, PartialEq, Eq)] +pub struct InvalidBufferError; + +pub struct TlvBuilderFinisher { + start_ptr: *const u32, + end_ptr: *const u32, +} +impl TlvBuilderFinisher { + #[inline(always)] + pub fn into_words(self, buf: &[u32]) -> Result<&[u32], InvalidBufferError> { + // This is all a sanity check that ensures that the user passed the + // correct buffer in. Hopefully the optimizer is smart enough to remove + // it. + if self.start_ptr != buf.as_ptr() + || self.end_ptr > buf.as_ptr_range().end + || self.end_ptr < self.start_ptr + { + return Err(InvalidBufferError); + } + let len = ((self.end_ptr as usize) - (self.start_ptr as usize)) / 4; + Ok(&buf[..len]) + } + #[inline(always)] + pub fn into_bytes(self, buf: &[u32]) -> Result<&[u8], InvalidBufferError> { + Ok(self.into_words(buf)?.as_bytes()) + } +} diff --git a/shared/tlv/codegen.rs b/shared/tlv/codegen.rs new file mode 100644 index 00000000..8459564d --- /dev/null +++ b/shared/tlv/codegen.rs @@ -0,0 +1,1173 @@ +// Copyright 2026 Google LLC +// +// 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. + +use openprot_proto_reflect::TypeExt; +use openprot_tlv_proto_meta::BackingType; +use openprot_tlv_proto_meta::DescriptorProtoTlvExt; +use openprot_tlv_proto_meta::EnumDescriptorProtoTlvExt; +use openprot_tlv_proto_meta::FieldDescriptorProtoTlvExt; +use openprot_tlv_proto_meta::FieldLayout; +use openprot_tlv_proto_meta::NameStrExt; +use openprot_tlv_proto_meta::PackedPrimitiveType; +use openprot_tlv_proto_meta::PackedType; +use openprot_tlv_proto_meta::TlvLayout; +use openprot_tlv_proto_meta::find_enum_descriptor; +use openprot_tlv_proto_meta::find_message_descriptor; +use openprot_tlv_proto_meta::get_rust_struct_name; +use proc_macro2::Ident; +use proc_macro2::Literal; +use proc_macro2::Span; +use proc_macro2::TokenStream; +use protobuf::Parse; +use std::collections::HashMap; + +use anyhow::Context; +use anyhow::Result; + +// Re-export descriptor views +use descriptor_rust_proto::DescriptorProtoView; +use descriptor_rust_proto::FileDescriptorSet; + +use argh::FromArgs; + +#[derive(FromArgs)] +/// Protobuf-to-TLV code generator +struct Args { + #[argh(option)] + /// paths to descriptor sets + descriptor_set: Vec, + + #[argh(option)] + /// paths to direct proto files (only emit code for types defined in these files) + direct_proto: Vec, + + #[argh(option)] + /// path to direct descriptor set (only emit code for types in this descriptor set) + direct_descriptor_set: Option, + + #[argh(switch)] + /// emit code for all files in descriptor set (disables direct file filtering) + all_files: bool, + + #[argh(option)] + /// rust crates to import (e.g. crypto_tlv) + import_crate: Vec, + + #[argh(option)] + /// path to the output Rust file + output: std::path::PathBuf, + + #[argh(option)] + /// optional label of the diff test target + test_label: Option, +} + +fn main() -> Result<()> { + let args: Args = argh::from_env(); + + if args.descriptor_set.is_empty() { + anyhow::bail!("Missing --descriptor-set argument"); + } + + let mut desc_set = FileDescriptorSet::new(); + for path in &args.descriptor_set { + let bytes = std::fs::read(path) + .with_context(|| format!("Failed to read descriptor set from {}", path.display()))?; + let part = FileDescriptorSet::parse(&bytes).with_context(|| { + format!("Failed to parse FileDescriptorSet from {}", path.display()) + })?; + desc_set + .file_mut() + .extend(part.file().into_iter().map(|f| f.to_owned())); + } + + let mut direct_desc_names = std::collections::HashSet::new(); + if let Some(direct_ds_path) = &args.direct_descriptor_set { + let bytes = std::fs::read(direct_ds_path).with_context(|| { + format!( + "Failed to read direct descriptor set from {}", + direct_ds_path.display() + ) + })?; + let direct_set = FileDescriptorSet::parse(&bytes).with_context(|| { + format!( + "Failed to parse direct FileDescriptorSet from {}", + direct_ds_path.display() + ) + })?; + for file in direct_set.file() { + if let Ok(name) = file.name().to_str() { + direct_desc_names.insert(name.to_string()); + } + } + } + + let target_files: Vec<_> = user_files(&desc_set) + .filter(|f| { + let file_name = f.name().to_str().unwrap_or(""); + is_direct_file( + file_name, + &args.direct_proto, + &direct_desc_names, + args.all_files, + ) + }) + .collect(); + + let crates_to_import: std::collections::BTreeSet<_> = + args.import_crate.iter().cloned().collect(); + + let mut out = String::new(); + out.push_str("// Copyright 2026 Google LLC\n"); + out.push_str("//\n"); + out.push_str("// Licensed under the Apache License, Version 2.0 (the \"License\");\n"); + out.push_str("// you may not use this file except in compliance with the License.\n"); + out.push_str("// You may obtain a copy of the License at\n"); + out.push_str("//\n"); + out.push_str("// http://www.apache.org/licenses/LICENSE-2.0\n"); + out.push_str("//\n"); + out.push_str("// Unless required by applicable law or agreed to in writing, software\n"); + out.push_str("// distributed under the License is distributed on an \"AS IS\" BASIS,\n"); + out.push_str("// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n"); + out.push_str("// See the License for the specific language governing permissions and\n"); + out.push_str("// limitations under the License.\n\n"); + out.push_str("// Automatically generated by tlv_codegen. DO NOT EDIT.\n"); + if let Some(label) = &args.test_label { + out.push_str(&format!( + "// To regenerate, run:\n// bazel run {label}\n", + )); + } + out.push('\n'); + out.push_str("#![no_std]\n"); + out.push_str("#![allow(non_camel_case_types)]\n"); + out.push_str("#![allow(non_snake_case)]\n"); + out.push_str("#![allow(unused_imports)]\n"); + out.push_str("#![allow(dead_code)]\n\n"); + out.push_str("use bitfield_struct::bitfield;\n"); + out.push_str("use openprot_tlv::TlvFlags;\n"); + out.push_str("use openprot_tlv::TlvHeader;\n"); + out.push_str("use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout};\n"); + for crate_name in &crates_to_import { + out.push_str(&format!("pub use {}::*;\n", crate_name)); + } + out.push('\n'); + + // Traverse direct target files and collect all messages with their full package-relative names + let mut all_messages = Vec::new(); + for file in &target_files { + for msg in file.message_type() { + collect_messages_nested(msg, "", &mut all_messages); + } + } + + // Traverse direct target files and collect all enums with their full package-relative names + let mut all_enums = Vec::new(); + for file in &target_files { + let package = file.package().to_str().unwrap_or(""); + for en in file.enum_type() { + if en.raw_type().is_some() { + let full_name = if package.is_empty() { + en.name().to_str().unwrap_or("").to_string() + } else { + format!("{}.{}", package, en.name().to_str().unwrap_or("")) + }; + all_enums.push((en, full_name)); + } + } + for msg in file.message_type() { + collect_enums_nested(package, msg, &mut all_enums); + } + } + + // Generate code for all enums using the open-enum pattern + for (en, full_name) in &all_enums { + let tokens = generate_open_enum(*en, full_name, &desc_set); + out.push_str(&tokens.to_string()); + out.push_str("\n\n"); + } + + // Generate code for all messages that have TLV or BitPacked layouts + for (msg, full_type_name) in all_messages { + let has_fourcc = msg.tlv_fourcc().is_some(); + let has_bit_packed = msg.bit_packed()?.is_some(); + + if has_fourcc || has_bit_packed { + let layout = msg.tlv_layout_with_name(&full_type_name, &desc_set)?; + let tokens = if has_bit_packed { + generate_bit_packed(msg, &full_type_name, layout, &desc_set)? + } else { + generate_standard_tlv(msg, &full_type_name, layout, &desc_set)? + }; + out.push_str(&tokens.to_string()); + out.push_str("\n\n"); + } else { + // Also generate plain structs for any standard nested messages used inline + // that don't have their own tlv_fourcc. + if let Ok(layout @ TlvLayout::Standard { .. }) = + msg.tlv_layout_with_name(&full_type_name, &desc_set) + { + let tokens = generate_plain_struct(msg, &full_type_name, layout, &desc_set)?; + out.push_str(&tokens.to_string()); + out.push_str("\n\n"); + } + } + } + + let formatted = format_code(&out).context("Failed to format generated code")?; + + std::fs::write(&args.output, formatted).with_context(|| { + format!( + "Failed to write generated code to {}", + args.output.display() + ) + })?; + Ok(()) +} + +fn format_code(code: &str) -> Result { + use std::io::Write; + use std::process::Command; + use std::process::Stdio; + + let rustfmt_path = std::env::var("RUSTFMT").unwrap_or_else(|_| "rustfmt".to_string()); + let mut child = Command::new(rustfmt_path) + .args(["--edition", "2024", "--config-path", "/dev/null"]) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .context("Failed to spawn rustfmt process")?; + + child + .stdin + .take() + .context("Failed to open stdin for rustfmt")? + .write_all(code.as_bytes())?; + + let output = child.wait_with_output()?; + if !output.status.success() { + anyhow::bail!( + "rustfmt failed:\n{}", + String::from_utf8_lossy(&output.stderr) + ); + } + Ok(String::from_utf8(output.stdout)?) +} + +fn collect_messages_nested<'a>( + msg: DescriptorProtoView<'a>, + parent_name: &str, + messages: &mut Vec<(DescriptorProtoView<'a>, String)>, +) { + let full_name = if parent_name.is_empty() { + msg.name_str().to_string() + } else { + format!("{}.{}", parent_name, msg.name_str()) + }; + messages.push((msg, full_name.clone())); + for nested in msg.nested_type() { + collect_messages_nested(nested, &full_name, messages); + } +} + +fn collect_enums_nested<'a>( + parent_path: &str, + msg: DescriptorProtoView<'a>, + out: &mut Vec<(descriptor_rust_proto::EnumDescriptorProtoView<'a>, String)>, +) { + let msg_name = msg.name_str(); + let current_path = if parent_path.is_empty() { + msg_name.to_string() + } else { + format!("{}.{}", parent_path, msg_name) + }; + + for en in msg.enum_type() { + if en.raw_type().is_some() { + let full_name = format!("{}.{}", current_path, en.name().to_str().unwrap_or("")); + out.push((en, full_name)); + } + } + + for nested in msg.nested_type() { + collect_enums_nested(¤t_path, nested, out); + } +} + +fn find_common_prefix_to_strip(variant_names: &[String]) -> Option { + if variant_names.len() <= 1 { + return None; + } + + // 1. Find the longest common prefix + let mut common = variant_names[0].clone(); + for name in variant_names.iter().skip(1) { + let mut len = 0; + for (c1, c2) in common.chars().zip(name.chars()) { + if c1 == c2 { + len += c1.len_utf8(); + } else { + break; + } + } + common.truncate(len); + } + + // 2. Find the last underscore in the common prefix + let last_under = common.rfind('_')?; + let prefix = &common[..=last_under]; // Includes the '_' + + if prefix.is_empty() { + return None; + } + + // 3. Validate that all stripped names are valid Rust identifiers + for name in variant_names { + let stripped = name.strip_prefix(prefix)?; + if stripped.is_empty() { + return None; + } + // Rust identifiers cannot start with a digit + if stripped.chars().next().unwrap().is_ascii_digit() { + return None; + } + } + + Some(prefix.to_string()) +} + +fn generate_open_enum( + en: descriptor_rust_proto::EnumDescriptorProtoView<'_>, + full_name: &str, + desc_set: &FileDescriptorSet, +) -> TokenStream { + let rust_name = get_rust_struct_name(full_name, desc_set); + let ident = get_ident(&rust_name); + + let backing_type_str = en.raw_type().unwrap_or_else(|| "i32".to_string()); + let backing_type: TokenStream = backing_type_str.parse().unwrap(); + + let mut variant_names = Vec::new(); + for val in en.value() { + variant_names.push(val.name().to_str().unwrap_or("").to_string()); + } + + let prefix_to_strip = find_common_prefix_to_strip(&variant_names); + + let mut variants = Vec::new(); + let mut debug_matches = Vec::new(); + let mut seen_values = std::collections::HashSet::new(); + for val in en.value() { + let val_name_raw = val.name().to_str().unwrap_or(""); + let val_name = if let Some(prefix) = &prefix_to_strip { + val_name_raw.strip_prefix(prefix).unwrap_or(val_name_raw) + } else { + val_name_raw + }; + let val_number = val.number(); + let val_lit = proc_macro2::Literal::i32_unsuffixed(val_number); + let variant_ident = get_ident(val_name); + + variants.push(quote::quote! { + pub const #variant_ident: Self = Self(#val_lit); + }); + + if seen_values.insert(val_number) { + let val_name_str = variant_ident.to_string(); + debug_matches.push(quote::quote! { + Self::#variant_ident => write!(f, #val_name_str), + }); + } + } + + quote::quote! { + #[derive(Clone, Copy, PartialEq, Eq, Hash, Default, FromBytes, IntoBytes, Immutable, KnownLayout)] + #[repr(transparent)] + pub struct #ident(pub #backing_type); + + impl #ident { + #(#variants)* + } + + impl ::core::fmt::Debug for #ident { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + match *self { + #(#debug_matches)* + other => write!(f, "{}({})", stringify!(#ident), other.0), + } + } + } + } +} + +fn get_ident(name: &str) -> Ident { + const KEYWORDS: &[&str] = &[ + "as", "break", "const", "continue", "crate", "else", "enum", "extern", "false", "fn", + "for", "if", "impl", "in", "let", "loop", "match", "mod", "move", "mut", "pub", "ref", + "return", "self", "Self", "static", "struct", "super", "trait", "true", "type", "unsafe", + "use", "where", "while", "async", "await", "dyn", + ]; + if KEYWORDS.contains(&name) { + Ident::new_raw(name, Span::call_site()) + } else { + Ident::new(name, Span::call_site()) + } +} + +fn get_bitfield_type( + bit_len: usize, + proto_type: descriptor_rust_proto::field_descriptor_proto::Type, +) -> &'static str { + let is_signed = proto_type + .primitive_info() + .is_some_and(|info| info.is_signed); + if bit_len == 1 { + "bool" + } else if bit_len <= 8 { + if is_signed { "i8" } else { "u8" } + } else if bit_len <= 16 { + if is_signed { "i16" } else { "u16" } + } else if bit_len <= 32 { + if is_signed { "i32" } else { "u32" } + } else { + if is_signed { "i64" } else { "u64" } + } +} + +fn generate_bit_packed( + msg: DescriptorProtoView<'_>, + full_type_name: &str, + layout: TlvLayout, + desc_set: &FileDescriptorSet, +) -> Result { + let msg_name = get_rust_struct_name(full_type_name, desc_set); + let msg_ident = get_ident(&msg_name); + + let (fields, backing) = match layout { + TlvLayout::BitPacked { fields, backing } => (fields, backing), + _ => anyhow::bail!("Expected BitPacked layout"), + }; + + let backing_ident = match backing { + BackingType::U8 => get_ident("u8"), + BackingType::U16 => get_ident("u16"), + BackingType::U32 => get_ident("u32"), + }; + + let mut field_tokens = Vec::new(); + let mut sorted_fields = Vec::new(); + for field in msg.field() { + let num = field.number() as u32; + if let Some(packed_field) = fields.get(&num) { + sorted_fields.push((field, packed_field)); + } + } + sorted_fields.sort_by_key(|f| f.1.bit_offset); + + let mut current_bit = 0; + for (field, packed_field) in sorted_fields { + let bit_offset = packed_field.bit_offset; + let bit_len = packed_field.bit_len; + + if bit_offset > current_bit { + let gap = bit_offset - current_bit; + let gap_lit = Literal::usize_unsuffixed(gap); + field_tokens.push(quote::quote! { + #[bits(#gap_lit)] + __ : (), + }); + } + + let field_ident = get_ident(field.name_str()); + let rust_type_str = get_bitfield_type(bit_len, field.r#type()); + let rust_type: TokenStream = rust_type_str.parse().unwrap(); + let bit_len_lit = Literal::usize_unsuffixed(bit_len); + + field_tokens.push(quote::quote! { + #[bits(#bit_len_lit)] + pub #field_ident: #rust_type, + }); + + current_bit = bit_offset + bit_len; + } + + let total_bits = backing.bits(); + if total_bits > current_bit { + let gap = total_bits - current_bit; + let gap_lit = Literal::usize_unsuffixed(gap); + field_tokens.push(quote::quote! { + #[bits(#gap_lit)] + __ : (), + }); + } + + Ok(quote::quote! { + #[bitfield(#backing_ident)] + #[derive(PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] + pub struct #msg_ident { + #(#field_tokens)* + } + }) +} + +fn generate_const_assertions( + msg_ident: &Ident, + fields: &HashMap, + fixed_struct_size: usize, + msg: DescriptorProtoView<'_>, +) -> TokenStream { + let mut field_assertions = Vec::new(); + for field in msg.field() { + let num = field.number() as u32; + if let Some(FieldLayout::Fixed { offset, .. }) = fields.get(&num) { + let field_ident = get_ident(field.name_str()); + field_assertions.push(quote::quote! { + assert!(::core::mem::offset_of!(#msg_ident, #field_ident) == #offset); + }); + } + } + + quote::quote! { + const _: () = { + #(#field_assertions)* + assert!(::core::mem::align_of::<#msg_ident>() <= 4); + assert!(::core::mem::size_of::<#msg_ident>() == #fixed_struct_size); + }; + } +} + +fn generate_standard_tlv( + msg: DescriptorProtoView<'_>, + full_type_name: &str, + layout: TlvLayout, + desc_set: &FileDescriptorSet, +) -> Result { + let msg_name = get_rust_struct_name(full_type_name, desc_set); + let msg_ident = get_ident(&msg_name); + + let (fields, fixed_struct_size) = match layout { + TlvLayout::Standard { + fields, + fixed_struct_size, + } => (fields, fixed_struct_size), + _ => anyhow::bail!("Expected Standard layout"), + }; + + let has_dynamic_tlvs = fields + .values() + .any(|f| matches!(f, FieldLayout::Tlv { .. })); + + let ext_type = if has_dynamic_tlvs { + quote::quote! { ::openprot_tlv::TlvData } + } else { + quote::quote! { [u8] } + }; + + let make_ext_body = if has_dynamic_tlvs { + quote::quote! { ::openprot_tlv::TlvData::overlay(extra) } + } else { + quote::quote! { ::openprot_tlv::unpad(header, extra) } + }; + + let header_arg = if has_dynamic_tlvs { + quote::quote! { _header } + } else { + quote::quote! { header } + }; + + let fourcc = msg.tlv_fourcc().unwrap(); + let fourcc_lit = Literal::byte_string(&fourcc); + + let field_tokens = generate_struct_fields(msg, &fields, fixed_struct_size, desc_set)?; + + let (mut signatures, mut accessors) = generate_sub_tlv_accessors(msg, &fields, desc_set)?; + let (enum_decls, oneof_sigs, oneof_accs) = generate_oneofs(msg, &msg_name, desc_set)?; + signatures.extend(oneof_sigs); + accessors.extend(oneof_accs); + + let accessors_impl = if !accessors.is_empty() { + let trait_ident = get_ident(&format!("{}Ext", msg_name)); + quote::quote! { + pub trait #trait_ident<'a> { + #(#signatures)* + } + + impl<'a> #trait_ident<'a> for ::openprot_tlv::TlvItem<'a, #msg_ident> { + #(#accessors)* + } + } + } else { + quote::quote! {} + }; + + let mut visited = std::collections::HashSet::new(); + let derives = if has_floats(msg, desc_set, &mut visited) { + quote::quote! { #[derive(Debug, Clone, Copy, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)] } + } else { + quote::quote! { #[derive(Debug, Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] } + }; + + let const_assertions = generate_const_assertions(&msg_ident, &fields, fixed_struct_size, msg); + + Ok(quote::quote! { + #derives + #[repr(C)] + pub struct #msg_ident { + #(#field_tokens)* + } + + #const_assertions + + impl ::core::default::Default for #msg_ident { + fn default() -> Self { + ::zerocopy::FromZeros::new_zeroed() + } + } + + impl ::openprot_tlv::TlvObject for #msg_ident { + type Extension = #ext_type; + const TAG: u32 = u32::from_le_bytes(*#fourcc_lit); + fn make_ext<'a>(#header_arg: &::openprot_tlv::TlvHeader, extra: &'a [u32]) -> &'a Self::Extension { + #make_ext_body + } + } + + #(#enum_decls)* + + #accessors_impl + }) +} + +fn generate_sub_tlv_accessors( + msg: DescriptorProtoView<'_>, + fields: &HashMap, + desc_set: &FileDescriptorSet, +) -> Result<(Vec, Vec)> { + use descriptor_rust_proto::field_descriptor_proto::Label; + + let mut signatures = Vec::new(); + let mut accessors = Vec::new(); + for field in msg.field() { + let num = field.number() as u32; + if let Some(layout) = fields.get(&num) { + match layout { + FieldLayout::Tlv { .. } => { + let field_ident = get_ident(field.name_str()); + + let type_name_raw = field.type_name().to_str().unwrap_or(""); + let type_name = type_name_raw.strip_prefix('.').unwrap_or(type_name_raw); + let child_struct_name = get_rust_struct_name(type_name, desc_set); + let child_ident = get_ident(&child_struct_name); + + let is_repeated = field.label() == Label::Repeated; + + if is_repeated { + signatures.push(quote::quote! { + fn #field_ident(&self) -> ::openprot_tlv::TlvIterator<'a, #child_ident>; + }); + accessors.push(quote::quote! { + #[inline(always)] + fn #field_ident(&self) -> ::openprot_tlv::TlvIterator<'a, #child_ident> { + ::openprot_tlv::TlvQuery::ext(self).iter::<#child_ident>() + } + }); + } else { + signatures.push(quote::quote! { + fn #field_ident(&self) -> Option<::openprot_tlv::TlvItem<'a, #child_ident>>; + }); + accessors.push(quote::quote! { + #[inline(always)] + fn #field_ident(&self) -> Option<::openprot_tlv::TlvItem<'a, #child_ident>> { + ::openprot_tlv::TlvQuery::ext(self).iter::<#child_ident>().next() + } + }); + } + } + FieldLayout::ExtraData => { + let field_ident = get_ident(field.name_str()); + let kind = field.r#type(); + if kind == descriptor_rust_proto::field_descriptor_proto::Type::String { + signatures.push(quote::quote! { + fn #field_ident(&self) -> &'a ::bstr::BStr; + }); + accessors.push(quote::quote! { + #[inline(always)] + fn #field_ident(&self) -> &'a ::bstr::BStr { + use ::openprot_tlv::TlvQuery; + ::bstr::BStr::new(self.ext()) + } + }); + } else { + signatures.push(quote::quote! { + fn #field_ident(&self) -> &'a [u8]; + }); + accessors.push(quote::quote! { + #[inline(always)] + fn #field_ident(&self) -> &'a [u8] { + use ::openprot_tlv::TlvQuery; + self.ext() + } + }); + } + } + _ => {} + } + } + } + Ok((signatures, accessors)) +} + +fn generate_plain_struct( + msg: DescriptorProtoView<'_>, + full_type_name: &str, + layout: TlvLayout, + desc_set: &FileDescriptorSet, +) -> Result { + let msg_name = get_rust_struct_name(full_type_name, desc_set); + let msg_ident = get_ident(&msg_name); + + let (fields, fixed_struct_size) = match layout { + TlvLayout::Standard { + fields, + fixed_struct_size, + } => (fields, fixed_struct_size), + _ => anyhow::bail!("Expected Standard layout"), + }; + + let field_tokens = generate_struct_fields(msg, &fields, fixed_struct_size, desc_set)?; + + let mut visited = std::collections::HashSet::new(); + let derives = if has_floats(msg, desc_set, &mut visited) { + quote::quote! { #[derive(Debug, Clone, Copy, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)] } + } else { + quote::quote! { #[derive(Debug, Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] } + }; + + let const_assertions = generate_const_assertions(&msg_ident, &fields, fixed_struct_size, msg); + + Ok(quote::quote! { + #derives + #[repr(C)] + pub struct #msg_ident { + #(#field_tokens)* + } + + #const_assertions + + impl ::core::default::Default for #msg_ident { + fn default() -> Self { + ::zerocopy::FromZeros::new_zeroed() + } + } + }) +} + +fn to_aligned_rust_type(pt: &PackedType) -> String { + match pt { + PackedType::Primitive(prim) => match prim { + PackedPrimitiveType::U64 => "::openprot_aligned::U64Align4".to_string(), + PackedPrimitiveType::I64 => "::zerocopy::byteorder::native_endian::I64".to_string(), + PackedPrimitiveType::F64 => "::zerocopy::byteorder::native_endian::F64".to_string(), + _ => prim.to_string(), + }, + PackedType::Array { elem, len } => { + let elem_str = match elem { + PackedPrimitiveType::U64 => "::openprot_aligned::U64Align4".to_string(), + PackedPrimitiveType::I64 => "::zerocopy::byteorder::native_endian::I64".to_string(), + PackedPrimitiveType::F64 => "::zerocopy::byteorder::native_endian::F64".to_string(), + _ => elem.to_string(), + }; + format!("[{}; {}]", elem_str, len) + } + PackedType::Struct { name, .. } => name.clone(), + } +} + +fn generate_struct_fields( + msg: DescriptorProtoView<'_>, + fields: &HashMap, + fixed_struct_size: usize, + desc_set: &FileDescriptorSet, +) -> Result> { + let mut fixed_fields = Vec::new(); + for field in msg.field() { + let num = field.number() as u32; + if let Some(FieldLayout::Fixed { + offset, + packed_type, + }) = fields.get(&num) + { + let rust_type_str = + if field.r#type() == descriptor_rust_proto::field_descriptor_proto::Type::Enum { + let type_name_raw = field.type_name().to_str().unwrap_or(""); + let type_name = type_name_raw.strip_prefix('.').unwrap_or(type_name_raw); + + let has_raw_type = find_enum_descriptor(desc_set, type_name) + .and_then(|e| e.raw_type()) + .is_some(); + + if has_raw_type && field.type_option().is_none() { + get_rust_struct_name(type_name, desc_set) + } else { + to_aligned_rust_type(packed_type) + } + } else { + to_aligned_rust_type(packed_type) + }; + + fixed_fields.push((field, *offset, packed_type.size(), rust_type_str)); + } + } + fixed_fields.sort_by_key(|f| f.1); + + let mut field_tokens = Vec::new(); + let mut current_offset = 0; + let mut pad_count = 0; + for (field, offset, size, rust_type_str) in fixed_fields { + if offset > current_offset { + let gap = offset - current_offset; + let pad_ident = get_ident(&format!("__pad{}", pad_count)); + field_tokens.push(quote::quote! { + pub #pad_ident: [u8; #gap], + }); + pad_count += 1; + } + + let field_ident = get_ident(field.name_str()); + let rust_type: TokenStream = rust_type_str.parse().unwrap(); + + field_tokens.push(quote::quote! { + pub #field_ident: #rust_type, + }); + + current_offset = offset + size; + } + + if fixed_struct_size > current_offset { + let gap = fixed_struct_size - current_offset; + let pad_ident = get_ident(&format!("__pad{}", pad_count)); + field_tokens.push(quote::quote! { + pub #pad_ident: [u8; #gap], + }); + } + + Ok(field_tokens) +} + +fn has_floats<'a>( + msg: descriptor_rust_proto::DescriptorProtoView<'a>, + desc_set: &'a FileDescriptorSet, + visited: &mut std::collections::HashSet, +) -> bool { + use descriptor_rust_proto::field_descriptor_proto::Type; + + let msg_name = msg.name_str().to_string(); + if !visited.insert(msg_name) { + return false; + } + + for field in msg.field() { + if field.r#type() == Type::Float || field.r#type() == Type::Double { + return true; + } + + if field.r#type() == Type::Message || field.r#type() == Type::Group { + let type_name_raw = field.type_name().to_str().unwrap_or(""); + let type_name = type_name_raw.strip_prefix('.').unwrap_or(type_name_raw); + if let Some(child_desc) = find_message_descriptor(desc_set, type_name) { + if has_floats(child_desc, desc_set, visited) { + return true; + } + } + } + } + + false +} + +fn to_camel_case(s: &str) -> String { + s.split('_') + .map(|word| { + let mut c = word.chars(); + match c.next() { + None => String::new(), + Some(f) => f.to_uppercase().collect::() + c.as_str(), + } + }) + .collect::() +} + +fn generate_oneofs( + msg: DescriptorProtoView<'_>, + msg_name: &str, + desc_set: &FileDescriptorSet, +) -> Result<(Vec, Vec, Vec)> { + let mut enum_decls = Vec::new(); + let mut signatures = Vec::new(); + let mut accessors = Vec::new(); + + for (index, oneof_decl) in msg.oneof_decl().iter().enumerate() { + let oneof_name = oneof_decl.name().to_str().unwrap_or(""); + if oneof_name.is_empty() { + continue; + } + + let mut oneof_fields = Vec::new(); + for field in msg.field() { + if field.has_oneof_index() && field.oneof_index() == index as i32 { + oneof_fields.push(field); + } + } + + if oneof_fields.is_empty() { + continue; + } + + let oneof_name_camel = to_camel_case(oneof_name); + let enum_name = format!("{}{}", msg_name, oneof_name_camel); + let enum_ident = get_ident(&enum_name); + + let mut variants = Vec::new(); + let mut checks = Vec::new(); + + for field in &oneof_fields { + let variant_name = to_camel_case(field.name_str()); + let variant_ident = get_ident(&variant_name); + + let type_name_raw = field.type_name().to_str().unwrap_or(""); + let type_name = type_name_raw.strip_prefix('.').unwrap_or(type_name_raw); + let child_struct_name = get_rust_struct_name(type_name, desc_set); + let child_ident = get_ident(&child_struct_name); + + variants.push(quote::quote! { + #variant_ident(::openprot_tlv::TlvItem<'a, #child_ident>), + }); + + checks.push(quote::quote! { + if let Some(val) = item.cast::<#child_ident>() { + return Some(#enum_ident::#variant_ident(val)); + } + }); + } + + enum_decls.push(quote::quote! { + pub enum #enum_ident<'a> { + #(#variants)* + } + }); + + let accessor_ident = get_ident(oneof_name); + signatures.push(quote::quote! { + fn #accessor_ident(&self) -> Option<#enum_ident<'a>>; + }); + + accessors.push(quote::quote! { + #[inline(always)] + fn #accessor_ident(&self) -> Option<#enum_ident<'a>> { + use ::openprot_tlv::TlvQuery; + for item in ::openprot_tlv::TlvQuery::ext(self).iter::<::openprot_tlv::TlvAny>() { + #(#checks)* + } + None + } + }); + } + + Ok((enum_decls, signatures, accessors)) +} + +/// Helper iterator filtering out built-in `google.protobuf` descriptor schema files. +fn user_files<'a>( + desc_set: &'a FileDescriptorSet, +) -> impl Iterator> { + desc_set.file().into_iter().filter(|f| { + !f.package() + .to_str() + .unwrap_or("") + .starts_with("google.protobuf") + }) +} + +fn matches_path(full_path: &str, target: &str) -> bool { + let full_path = full_path.trim(); + let target = target.trim(); + if full_path == target { + return true; + } + if let Some(stripped) = full_path.strip_suffix(target) { + if stripped.ends_with('/') || stripped.ends_with('\\') { + return true; + } + } + if let Some(stripped) = target.strip_suffix(full_path) { + if stripped.ends_with('/') || stripped.ends_with('\\') { + return true; + } + } + false +} + +fn is_direct_file( + file_name: &str, + direct_protos: &[std::path::PathBuf], + direct_desc_names: &std::collections::HashSet, + all_files: bool, +) -> bool { + if all_files { + return true; + } + if direct_desc_names.contains(file_name) { + return true; + } + for dp in direct_protos { + let dp_str = dp.to_string_lossy(); + if matches_path(file_name, &dp_str) { + return true; + } + } + false +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_matches_path() { + assert!(matches_path( + "path/to/my_message.proto", + "external/repo+/path/to/my_message.proto" + )); + assert!(matches_path( + "path/to/my_message.proto", + "../repo+/path/to/my_message.proto" + )); + assert!(matches_path( + "path/to/my_message.proto", + "path/to/my_message.proto" + )); + assert!(matches_path("path/to/my_message.proto", "my_message.proto")); + assert!(!matches_path("path/to/my_message.proto", "other.proto")); + assert!(!matches_path( + "path/to/not_my_message.proto", + "my_message.proto" + )); + } + + #[test] + fn test_is_direct_file() { + let direct_protos = vec![std::path::PathBuf::from("path/to/my_message.proto")]; + let mut direct_desc = std::collections::HashSet::new(); + direct_desc.insert("desc_message.proto".to_string()); + + // Matching direct_protos + assert!(is_direct_file( + "path/to/my_message.proto", + &direct_protos, + &direct_desc, + false + )); + assert!(is_direct_file( + "external/repo+/path/to/my_message.proto", + &direct_protos, + &direct_desc, + false + )); + + // Matching direct_desc_names + assert!(is_direct_file( + "desc_message.proto", + &direct_protos, + &direct_desc, + false + )); + + // Not matching + assert!(!is_direct_file( + "other.proto", + &direct_protos, + &direct_desc, + false + )); + + // Empty direct_protos and direct_desc_names with all_files = false (aggregating target) + let empty_protos = vec![]; + let empty_desc = std::collections::HashSet::new(); + assert!(!is_direct_file( + "path/to/my_message.proto", + &empty_protos, + &empty_desc, + false + )); + + // all_files = true overrides and matches everything + assert!(is_direct_file( + "path/to/my_message.proto", + &empty_protos, + &empty_desc, + true + )); + assert!(is_direct_file( + "other.proto", + &empty_protos, + &empty_desc, + true + )); + } + + #[test] + fn test_find_common_prefix_to_strip() { + let cases = vec![ + ( + vec![ + "EU_STATE_IDLE", + "EU_STATE_BUSY", + "EU_STATE_STALLED", + "EU_STATE_LARGE", + ], + Some("EU_STATE_"), + ), + ( + vec!["CPU_STATE_OFFLINE", "CPU_STATE_ACTIVE", "CPU_STATE_SLEEP"], + Some("CPU_STATE_"), + ), + ( + vec!["EU_STATE_0", "EU_STATE_1"], + None, // Stripping EU_STATE_ would leave "0", "1" which are invalid identifiers starting with a digit. + ), + ( + vec!["EU_STATE", "EU_STATE_BUSY"], + Some("EU_"), // Stripping EU_ leaves STATE and STATE_BUSY (valid). + ), + ( + vec!["PREFIXA", "PREFIXB"], + None, // No underscore in the common prefix "PREFIX". + ), + ( + vec!["A", "B", "C"], + None, // No common prefix. + ), + ( + vec!["ONLY_ONE"], + None, // Single element should not strip anything. + ), + ]; + + for (input, expected) in cases { + let string_input: Vec = input.into_iter().map(|s| s.to_string()).collect(); + let result = find_common_prefix_to_strip(&string_input); + assert_eq!(result.as_deref(), expected); + } + } +} diff --git a/shared/tlv/codegen_test.rs b/shared/tlv/codegen_test.rs new file mode 100644 index 00000000..38c0386a --- /dev/null +++ b/shared/tlv/codegen_test.rs @@ -0,0 +1,81 @@ +// Copyright 2026 Google LLC +// +// 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. + +// Integration test verifying the correctness of the generated TLV structs. + +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] + +#[test] +fn test_generated_code_compiles_and_aligns() { + // If this test compiles and runs, it means our codegen generated completely valid Rust code. + // Let's assert the exact size of the generated structs to verify alignment and padding logic: + + assert_eq!(core::mem::size_of::(), 44); + assert_eq!(core::mem::size_of::(), 12); + assert_eq!(core::mem::size_of::(), 12); + assert_eq!(core::mem::size_of::(), 8); + assert_eq!(core::mem::size_of::(), 16); + assert_eq!(core::mem::size_of::(), 8); + assert_eq!(core::mem::size_of::(), 4); + assert_eq!(core::mem::size_of::(), 2); + assert_eq!(core::mem::size_of::(), 1); + assert_eq!(core::mem::size_of::(), 8); + assert_eq!( + core::mem::size_of::(), + 4 + ); + assert_eq!( + core::mem::size_of::(), + 4 + ); + assert_eq!(core::mem::size_of::(), 12); + assert_eq!(core::mem::size_of::(), 100); +} + +#[test] +fn test_imported_crates_compile_and_reexport() { + // 1. Base crate + assert_eq!(core::mem::size_of::(), 8); + assert_eq!( + import_base_tlv::BaseStatus::OK, + import_base_tlv::BaseStatus(1) + ); + + // 2. Simple import (import_simple depends on import_base) + assert_eq!(core::mem::size_of::(), 8); + // BaseStatus is accessible through import_simple_tlv re-export + assert_eq!( + import_simple_tlv::BaseStatus::OK, + import_base_tlv::BaseStatus::OK + ); + + // 3. Multi-layered import (import_layered depends on import_simple and import_base) + assert_eq!( + core::mem::size_of::(), + 4 + ); + assert_eq!(core::mem::size_of::(), 8); + assert_eq!(core::mem::size_of::(), 8); + + // 4. "Pure" import aggregator target (no srcs, only deps) + // Re-exports all dependencies without emitting duplicate types + assert_eq!(core::mem::size_of::(), 8); + assert_eq!(core::mem::size_of::(), 8); + assert_eq!(core::mem::size_of::(), 4); + assert_eq!( + import_pure_tlv::BaseStatus::OK, + import_base_tlv::BaseStatus::OK + ); +} diff --git a/shared/tlv/lib.rs b/shared/tlv/lib.rs new file mode 100644 index 00000000..8570bc51 --- /dev/null +++ b/shared/tlv/lib.rs @@ -0,0 +1,1158 @@ +// Copyright 2026 Google LLC +// +// 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. + +//! A simple, zero-copy framework for working with Type-Length-Value (TLV) objects. +//! +//! This crate provides tools to overlay structured views onto raw byte buffers, +//! allowing efficient parsing and creation of TLV structures without unnecessary +//! allocations or copies. +//! +//! # Core Concepts +//! +//! * [`TlvHeader`]: The common prefix for all TLV objects, containing a 4-byte `tag` and a 2-byte `length` (in 32-bit words). +//! * [`TlvContainer`]: The contiguous representation of a single TLV object, containing its [`TlvHeader`] and raw payload words. +//! * [`TlvObject`]: A trait representing a typed TLV structure. The [`tlv_struct!`] macro +//! is used to define these structs and implement this trait. +//! * [`TlvData`]: A container representing a sequence of TLVs. It can be used to overlay +//! on a raw buffer and iterate over the contained TLVs. +//! * [`TlvItem`]: The parsed representation of a TLV, containing a reference to the [`TlvContainer`], +//! the parsed object, and any extra data words. +//! * [`TlvQuery`]: Extension trait for `TlvItem` to allow casting to specific `TlvObject` types +//! and accessing extension data. +//! +//! # Defining TLV Structures +//! +//! Use the [`tlv_struct!`] macro to define your TLV structures. You must specify: +//! 1. The name of the struct. +//! 2. The 4-byte tag as a byte literal (e.g., `*b"FOO_"`). +//! 3. The type of the extension: either `[u8]` (raw bytes) or `TlvData` (nested TLVs). +//! 4. The struct definition. +//! +//! ```rust +//! # use tlv::tlv_struct; +//! tlv_struct! { Foo, *b"FOO_", [u8], +//! #[derive(Debug)] +//! pub struct Foo { +//! pub x: u32, +//! pub y: [u8; 4], +//! } +//! } +//! ``` +//! +//! # Parsing and Iteration +//! +//! To parse a buffer, use [`TlvData::overlay`] and then iterate over it: +//! +//! ```rust +//! # use tlv::{TlvData, TlvAny}; +//! # use zerocopy::IntoBytes; +//! let buf: &[u32] = &[]; // Use u32 slice +//! let tlv_data = TlvData::overlay(buf); +//! for item in tlv_data.iter::() { +//! println!("Tag: {}, word_len: {}", item.container.header.tag, item.container.header.word_len); +//! } +//! ``` + +#![cfg_attr(not(test), no_std)] + +use bitfield_struct::bitfield; +use core::cmp::min; +use zerocopy::FromBytes; +use zerocopy::Immutable; +use zerocopy::IntoBytes; +use zerocopy::KnownLayout; +use zerocopy::transmute_ref; + +mod builder; + +pub use builder::InvalidBufferError; +pub use builder::TlvBuilder; +pub use builder::TlvBuilderFinisher; + +#[bitfield(u8)] +#[derive(PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +pub struct TlvFlags { + /// Number of trailing padding bytes (0-3) in the last word of the payload. + /// This is typically used when the variable-length extension is a slice of + /// u8 or u16 and the exact length is meaningful. + #[bits(2)] + pub padding_bytes: u8, + #[bits(6)] + __: (), +} + +/// Generic TLV header. +/// +/// Every TLV object starts with this header, defining its type (tag) and the length +/// of the payload (in 32-bit words) following the header. +#[derive( + Debug, Clone, Copy, PartialEq, Eq, Default, FromBytes, IntoBytes, Immutable, KnownLayout, +)] +#[repr(C)] +pub struct TlvHeader { + /// The tag identifying the type of the TLV object. Typically represented as a 4-character ASCII string (FourCC). + pub tag: u32, + /// The length of the data following this header, in 32-bit words. + pub word_len: u16, + /// Reserved; must be zero. + pub reserved: u8, + /// Flags; currently only includes the number of padding bytes. + pub flags: TlvFlags, +} + +impl TlvHeader { + fn ref_from_words_prefix(words: &[u32]) -> Option<(&TlvHeader, &[u32])> { + let (header_words, rest) = words.split_first_chunk::<{ word_size_of::() }>()?; + Some((transmute_ref!(header_words), rest)) + } + + #[inline(always)] + pub fn byte_len(&self) -> usize { + (usize::from(self.word_len) * 4).saturating_sub(usize::from(self.flags.padding_bytes())) + } +} + +/// Returns the size of the type `T` in 32-bit words, asserting at compile time +/// that the size of `T` is a multiple of 4 bytes. +#[inline(always)] +pub const fn word_size_of() -> usize { + const { + assert!( + core::mem::size_of::().is_multiple_of(4), + "TLV struct size must be a multiple of 4 bytes" + ); + } + core::mem::size_of::() / 4 +} + +/// Generic TLV container. +/// +/// This type represents a buffer containing zero or more TLV objects. It is a unsized +/// type and is typically used behind a reference (`&TlvData`) to overlay onto raw byte slices. +#[derive(FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct TlvData { + data: [u32], +} + +/// Zero-sized type that can represent any TLV. +/// +/// Used as a generic parameter for [`TlvData::iter`] or [`TlvQuery::cast`] when the +/// specific type is not known or any TLV type is acceptable. +#[derive(FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct TlvAny; + +impl TlvObject for TlvAny { + type Extension = [u8]; + const TAG: u32 = 0; + fn make_ext<'a>(header: &TlvHeader, extra: &'a [u32]) -> &'a Self::Extension { + unpad(header, extra) + } +} + +#[doc(hidden)] +pub fn unpad<'a>(header: &TlvHeader, extra: &'a [u32]) -> &'a [u8] { + let padding_bytes = usize::from(header.flags.padding_bytes()); + let extra_bytes = extra.as_bytes(); + extra_bytes + .get(..extra_bytes.len().saturating_sub(padding_bytes)) + .unwrap_or(&[]) +} + +#[derive(Debug, PartialEq, Eq)] +pub struct BadAlignmentError; + +impl TlvData { + /// Constructs a new `TlvData` reference by overlaying onto a word buffer. + #[inline(always)] + pub fn overlay(data: &[u32]) -> &Self { + transmute_ref!(data) + } + + /// Constructs a new `TlvData` reference by overlaying onto a byte buffer. + #[inline] + pub fn overlay_bytes(data: &[u8]) -> Result<&Self, BadAlignmentError> { + TlvData::ref_from_bytes_with_elems(data, data.len() / 4).map_err(|_| BadAlignmentError) + } + + /// Returns an iterator over the contained TLV objects of type `T`. + /// + /// If `T` is [`TlvAny`], the iterator will yield all TLVs. + /// Otherwise, it will only yield TLVs with a matching [`TlvObject::TAG`]. + pub fn iter<'a, T: TlvObject>(&'a self) -> TlvIterator<'a, T> { + TlvIterator { + data: &self.data, + target: core::marker::PhantomData, + } + } +} + +/// Iterator over TLV objects in a [`TlvData`] container. +pub struct TlvIterator<'a, T> { + data: &'a [u32], + target: core::marker::PhantomData, +} + +/// Contiguous representation of a single TLV object, comprising its header and payload. +/// +/// This dynamically sized type (DST) overlays directly onto the raw word buffer, +/// making it possible to access all bytes of the TLV record (e.g. for cryptographic hashing +/// or signature verification) via [`zerocopy::IntoBytes::as_bytes`]. +#[repr(C, align(4))] +#[derive(Debug, PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +pub struct TlvContainer { + /// The TLV header defining the type and payload length. + pub header: TlvHeader, + /// The raw payload words of the TLV object following the header. + pub payload: [u32], +} + +impl TlvContainer { + /// Returns a byte slice referencing the entire TLV container (header and payload). + /// + /// The returned slice is always a multiple of four bytes, regardless of the + /// padding flags in the header. + #[inline(always)] + pub fn as_bytes(&self) -> &[u8] { + IntoBytes::as_bytes(self) + } + + /// Returns a 32-bit word slice referencing the entire TLV container (header and payload). + #[inline(always)] + pub fn as_words(&self) -> &[u32] { + <[u32]>::ref_from_bytes(self.as_bytes()).unwrap_or(&[]) + } +} + +/// Item returned by [`TlvIterator`]. +pub struct TlvItem<'a, T: TlvObject> { + /// The TLV container containing the header and payload of the object. + pub container: &'a TlvContainer, + /// The TLV object itself (parsed struct). + pub data: &'a T, + /// Extra data after the parsed struct + pub extra: &'a [u32], +} + +impl<'a, T: TlvObject + 'a> Iterator for TlvIterator<'a, T> { + type Item = TlvItem<'a, T>; + + #[inline(always)] + fn next(&mut self) -> Option { + while !self.data.is_empty() { + let (header, _) = TlvHeader::ref_from_words_prefix(self.data)?; + let word_len = usize::from(header.word_len); + let total_words = word_size_of::() + word_len; + let (container_words, remain) = self.data.split_at_checked(total_words)?; + let container = + TlvContainer::ref_from_bytes_with_elems(container_words.as_bytes(), word_len) + .ok()?; + self.data = remain; + if container.header.tag != T::TAG && T::TAG != TlvAny::TAG { + continue; + } + let Some((data_words, extra)) = container.payload.split_at_checked(word_size_of::()) + else { + continue; + }; + let Ok(data) = T::ref_from_bytes(data_words.as_bytes()) else { + continue; + }; + return Some(TlvItem { + container, + data, + extra, + }); + } + None + } +} + +#[derive(Debug, PartialEq, Eq)] +pub struct TlvError; + +/// A trait for types that represent a TLV object. +/// +/// This trait is typically implemented using the [`tlv_struct!`] macro. +pub trait TlvObject: FromBytes + IntoBytes + Immutable + KnownLayout + Sized { + /// The extension type for this object. + /// + /// This is either `[u8]` for raw payload bytes or [`TlvData`] for nested TLV containers. + type Extension: ?Sized; + + /// The unique 4-byte tag identifying this TLV type. + const TAG: u32; + + /// Returns the tag of the object. + fn get_tag(&self) -> u32 { + Self::TAG + } + + /// Helper for making the extension object from raw bytes. + fn make_ext<'a>(header: &TlvHeader, extra: &'a [u32]) -> &'a Self::Extension; +} + +/// Extension trait providing helper methods on [`TlvItem`]. +/// +/// This trait allows casting a generic `TlvItem` (e.g., returned from `iter::()`) +/// to a specific `TlvObject` type, and retrieving the extension payload of a `TlvItem`. +pub trait TlvQuery<'a> { + /// The type of the extension associated with the item. + type Extension; + + /// Attempts to cast this item to a specific `TlvObject` type `U`. + /// + /// Returns `Some(TlvItem<'a, U>)` if the item's tag matches `U::TAG`, and the + /// data can be parsed as `U`. Otherwise, returns `None`. + fn cast(&self) -> Option>; + + /// Attempts to cast this item to a specific `TlvObject` type `U`, zero-extending + /// the data if the payload size (in words) is smaller than the struct. + /// + /// Returns `Some(U)` if the item's tag matches `U::TAG`. Otherwise, returns `None`. + /// + /// Note: To reduce the code-size cost of monomorphization bloat, this + /// operates strictly at 32-bit word granularity and ignores + /// `TlvFlags::padding_bytes`. Any trailing padding bytes in the last word + /// will be copied as-is into `U`. This should not be a problem, as a + /// compliant generator will set them to zero. + fn cast_zero_extended(&self) -> Option; + + /// Returns a reference to the extension payload of this item. + /// + /// If the `TlvObject` was defined with `[u8]` extension, this returns `&[u8]`. + /// If it was defined with `TlvData`, this returns `&TlvData` (for nested TLVs). + fn ext(&self) -> Self::Extension; +} + +impl<'a, T: TlvObject> TlvQuery<'a> for TlvItem<'a, T> { + type Extension = &'a T::Extension; + #[inline(always)] + fn cast(&self) -> Option> { + if self.container.header.tag == U::TAG { + let (data_words, extra) = self + .container + .payload + .split_at_checked(word_size_of::())?; + let data = U::ref_from_bytes(data_words.as_bytes()).ok()?; + Some(TlvItem { + container: self.container, + data, + extra, + }) + } else { + None + } + } + + fn cast_zero_extended(&self) -> Option { + if self.container.header.tag == U::TAG { + let mut result = U::new_zeroed(); + let payload_bytes = self.container.payload.as_bytes(); + let result_bytes = result.as_mut_bytes(); + let copy_limit = min(result_bytes.len(), payload_bytes.len()); + result_bytes[..copy_limit].copy_from_slice(&payload_bytes[..copy_limit]); + Some(result) + } else { + None + } + } + + fn ext(&self) -> Self::Extension { + T::make_ext(&self.container.header, self.extra) + } +} + +/// Defines a TLV struct and implements [`TlvObject`] for it. +/// +/// This macro generates a zero-copy representation of a TLV object. +/// +/// # Syntax +/// +/// ```ignore +/// tlv_struct! { Name, Tag, ExtensionType, +/// [attributes] +/// pub struct Name { +/// pub field1: Type1, +/// ... +/// } +/// } +/// ``` +/// +/// * `Name`: The identifier of the struct. +/// * `Tag`: A 4-byte tag represented as a dereferenced byte array pointer (e.g., `*b"FOO_"`). +/// * `ExtensionType`: The type of the extension following the struct fields. +/// * `[u8]`: Raw byte slice. +/// * [`TlvData`]: Nested TLV container. +/// * `definition`: The struct definition itself. Must use `#[repr(C)]` style layout +/// safety guarantees (handled by `zerocopy` derives automatically). +/// +/// # Examples +/// +/// Defining a struct with a raw byte extension: +/// +/// ```rust +/// # use tlv::tlv_struct; +/// tlv_struct! { Foo, *b"FOO_", [u8], +/// #[derive(Debug)] +/// pub struct Foo { +/// pub x: u32, +/// pub y: [u8; 4], +/// } +/// } +/// ``` +/// +/// Defining a struct with nested TLVs: +/// +/// ```rust +/// # use tlv::{tlv_struct, TlvData}; +/// tlv_struct! { Bar, *b"BAR_", TlvData, +/// #[derive(Debug)] +/// pub struct Bar { +/// pub version: u32, +/// } +/// } +/// ``` +#[macro_export] +macro_rules! tlv_struct { + // This macro doesn't follow "don't repeat yourself" very well. + + // Branch 1: Defines a TLV struct with a raw byte array ([u8]) extension. + ($name:ident, $tag:expr, [u8], $($definition:tt)*) => { + // Expand the user's struct definition. Automatically derive the zerocopy traits + // required to safely overlay this struct onto a byte buffer. + #[derive( + zerocopy::FromBytes, + zerocopy::IntoBytes, + zerocopy::Immutable, + zerocopy::KnownLayout, + )] + $($definition)* + + const _: () = { + assert!( + core::mem::size_of::<$name>() % 4 == 0, + "TLV struct size must be a multiple of 4 bytes" + ); + }; + + // Implement the TlvObject trait for the generated zero-copy struct. + impl $crate::TlvObject for $name { + type Extension = [u8]; + // Convert the tag from little-endian bytes to a u32 constant. + const TAG: u32 = u32::from_le_bytes($tag); + // Returns the raw byte slice as the extension type. + fn make_ext<'a>(header: &$crate::TlvHeader, extra: &'a [u32]) -> &'a Self::Extension { + $crate::unpad(header, extra) + } + + } + }; + + // Branch 2: Defines a TLV struct with a nested TLV container (TlvData) extension. + ($name:ident, $tag:expr, TlvData, $($definition:tt)*) => { + // Expand the user's struct definition. Same derivations as Branch 1. + #[derive( + zerocopy::FromBytes, + zerocopy::IntoBytes, + zerocopy::Immutable, + zerocopy::KnownLayout, + )] + $($definition)* + + const _: () = { + assert!( + core::mem::size_of::<$name>() % 4 == 0, + "TLV struct size must be a multiple of 4 bytes" + ); + }; + + // Implement the TlvObject trait for the generated zero-copy struct. + impl $crate::TlvObject for $name { + type Extension = $crate::TlvData; + // Convert the tag from little-endian bytes to a u32 constant. + const TAG: u32 = u32::from_le_bytes($tag); + // Overlays a TlvData view onto the remaining bytes to parse them as nested TLVs. + fn make_ext<'a>(_header: &$crate::TlvHeader, extra: &'a [u32]) -> &'a Self::Extension { + $crate::TlvData::overlay(extra) + } + } + }; +} + +#[cfg(test)] +mod tests { + use super::*; + + tlv_struct! { Parent, *b"PRNT", TlvData, + #[derive(Debug, PartialEq, Eq)] + pub struct Parent { + pub id: u32, + } + } + + tlv_struct! { Child, *b"CHLD", [u8], + #[derive(Debug, PartialEq, Eq)] + pub struct Child { + pub value: u32, + } + } + + tlv_struct! { MiniFoo, *b"FOO_", [u8], + #[derive(Debug, PartialEq, Eq)] + pub struct MiniFoo { + pub x: u32, + } + } + + tlv_struct! { Foo, *b"FOO_", [u8], + #[derive(Debug, PartialEq, Eq)] + pub struct Foo { + pub x: u32, + pub y: [u8; 4], + } + } + + tlv_struct! { Bar, *b"Bar_", TlvData, + #[derive(Debug, PartialEq, Eq)] + pub struct Bar { + pub n: u32, + } + } + + tlv_struct! { Baz, *b"Baz_", TlvData, + #[derive(Debug, PartialEq, Eq)] + pub struct Baz { + pub n: u32, + } + } + + tlv_struct! { Baf, *b"Baf_", TlvData, + #[derive(Debug, PartialEq, Eq)] + pub struct Baf { + pub a: [u32; 3], + } + } + + #[test] + fn test_build_and_parse_simple() { + let mut buf = [0u32; 32]; + + // Build Parent and Child + let (parent, mut builder) = TlvBuilder::new::(&mut buf).unwrap(); + parent.id = 100; + + let child = builder.add::().unwrap(); + child.value = 200; + + let result_bytes = builder.finish().into_bytes(&buf).unwrap(); + assert_eq!(result_bytes.len(), 24); + + // Parse back + let data = TlvData::overlay_bytes(result_bytes).unwrap(); + + let mut parent_iter = data.iter::(); + let parent_item = parent_iter.next().unwrap(); + assert!(parent_iter.next().is_none()); + + assert_eq!(parent_item.container.header.tag, Parent::TAG); + assert_eq!(parent_item.data.id, 100); + + // Parse Child from Parent's extension + let mut child_iter = parent_item.ext().iter::(); + let child_item = child_iter.next().expect("Should find Child"); + assert!(child_iter.next().is_none()); + + assert_eq!(child_item.container.header.tag, Child::TAG); + assert_eq!(child_item.data.value, 200); + } + + #[test] + fn test_parsing() { + // Flat binary buffer matching the layout in the test, with the updated header structure: + // - reserved byte at offset 6 is 0x00 + // - flags byte at offset 7 contains padding_bytes + let buf: [u8; 124] = [ + // Bar Header and data (tag: "Bar_", len: 29 words, n: 5) + b'B', b'a', b'r', b'_', 0x1d, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, + // Foo 1 Header and data (tag: "FOO_", len: 3 words, x: 20, y: [1, 2, 3, 4], ext: [5, 6, 7, 8]) + b'F', b'O', b'O', b'_', 0x03, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x01, 0x02, + 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + // Foo 2 Header and data (tag: "FOO_", len: 2 words, x: 30, y: [10, 20, 30, 40], ext = []) + b'F', b'O', b'O', b'_', 0x02, 0x00, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x0a, 0x14, + 0x1e, 0x28, + // Foo 3 Header and data (tag: "FOO_", len: 3 words, reserved: 0, flags: 1, x: 40, y: [11, 12, 13, 14], ext: [15, 16, 17], padded with one zero) + b'F', b'O', b'O', b'_', 0x03, 0x00, 0x00, 0x01, 0x28, 0x00, 0x00, 0x00, 0x0b, 0x0c, + 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x00, + // Foo 4 Header and data (tag: "FOO_", len: 4 words, reserved: 0, flags: 3, x: 50, y: [21, 22, 23, 24], ext: [25, 26, 27, 28, 29], padded with three zeros) + b'F', b'O', b'O', b'_', 0x04, 0x00, 0x00, 0x03, 0x32, 0x00, 0x00, 0x00, 21, 22, 23, 24, + 25, 26, 27, 28, 29, 0x00, 0x00, 0x00, + // Baz Header and data (tag: "Baz_", len: 1 word, n: 60) + b'B', b'a', b'z', b'_', 0x01, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x00, + // Baf Header and data (tag: "Baf_", len: 3 words, a: [237482734, 19263432, 81823823]) + b'B', b'a', b'f', b'_', 0x03, 0x00, 0x00, 0x00, 0xee, 0xb2, 0x27, 0x0e, 0xc8, 0xef, + 0x25, 0x01, 0x4f, 0x88, 0xe0, 0x04, + ]; + let buf_words: [u32; 31] = zerocopy::transmute!(buf); + + // Overlay TlvData and parse the outer container + let t = TlvData::overlay(&buf_words); + + let mut bar_iter = t.iter::(); + let bar_item = bar_iter.next().expect("Should have one Bar item"); + assert!(bar_iter.next().is_none()); + + assert_eq!(bar_item.container.header.tag, Bar::TAG); + assert_eq!(bar_item.container.header.word_len, 29); + assert_eq!(bar_item.data.n, 5); + assert_eq!(bar_item.container.as_bytes(), &buf); + assert_eq!(bar_item.container.as_words(), &buf_words); + + // Iterate over Bar's extensions + let mut ext_iter = bar_item.ext().iter::(); + + // First extension: Foo + let ext1 = ext_iter.next().expect("Should have first extension"); + assert_eq!(ext1.container.header.tag, Foo::TAG); + assert_eq!(ext1.container.header.word_len, 3); + assert_eq!(ext1.container.as_bytes(), &buf[12..32]); + assert_eq!(ext1.container.as_words(), &buf_words[3..8]); + let foo1 = ext1.cast::().expect("Should cast to Foo"); + assert_eq!(foo1.data.x, 20); + assert_eq!(foo1.data.y, [1, 2, 3, 4]); + assert_eq!(foo1.ext(), &[5, 6, 7, 8]); + + // Second extension: Foo + let ext2 = ext_iter.next().expect("Should have second extension"); + assert_eq!(ext2.container.header.tag, Foo::TAG); + assert_eq!(ext2.container.header.word_len, 2); + let foo2 = ext2.cast::().expect("Should cast to Foo"); + assert_eq!(foo2.data.x, 30); + assert_eq!(foo2.data.y, [10, 20, 30, 40]); + assert_eq!(foo2.ext(), &[] as &[u8]); + + // Third extension: Foo + let ext3 = ext_iter.next().expect("Should have third extension"); + assert_eq!(ext3.container.header.tag, Foo::TAG); + assert_eq!(ext3.container.header.word_len, 3); + let foo3 = ext3.cast::().expect("Should cast to Foo"); + assert_eq!(foo3.data.x, 40); + assert_eq!(foo3.data.y, [11, 12, 13, 14]); + assert_eq!(foo3.ext(), &[15, 16, 17]); + + // Fourth extension: Foo + let ext4 = ext_iter.next().expect("Should have fourth extension"); + assert_eq!(ext4.container.header.tag, Foo::TAG); + assert_eq!(ext4.container.header.word_len, 4); + let foo4 = ext4.cast::().expect("Should cast to Foo"); + assert_eq!(foo4.data.x, 50); + assert_eq!(foo4.data.y, [21, 22, 23, 24]); + assert_eq!(foo4.ext(), &[25, 26, 27, 28, 29]); + + // Fifth extension: Baz + let ext5 = ext_iter.next().expect("Should have fifth extension"); + assert_eq!(ext5.container.header.tag, Baz::TAG); + assert_eq!(ext5.container.header.word_len, 1); + let baz5 = ext5.cast::().expect("Should cast to Baz"); + assert_eq!(baz5.data.n, 60); + // Baz's extension type is TlvData, check that it is empty + assert!(baz5.ext().iter::().next().is_none()); + + // Sixth extension: Baf + let ext6 = ext_iter.next().expect("Should have sixth extension"); + assert_eq!(ext6.container.header.tag, Baf::TAG); + assert_eq!(ext6.container.header.word_len, 3); + let baf6 = ext6.cast::().expect("Should cast to Baf"); + assert_eq!(baf6.data.a, [237482734, 19263432, 81823823]); + + // No further extensions should exist + assert!(ext_iter.next().is_none()); + + // Iterate over only the Foos + let mut foo_iter = bar_item.ext().iter::(); + assert_eq!(foo_iter.next().unwrap().data, foo1.data); + assert_eq!(foo_iter.next().unwrap().data, foo2.data); + assert_eq!(foo_iter.next().unwrap().data, foo3.data); + assert_eq!(foo_iter.next().unwrap().data, foo4.data); + assert!(foo_iter.next().is_none()); + + // Iterate over only the Bazs + let mut baz_iter = bar_item.ext().iter::(); + assert_eq!(baz_iter.next().unwrap().data, baz5.data); + assert!(baz_iter.next().is_none()); + + // Iterate over only the Bafs + let mut baf_iter = bar_item.ext().iter::(); + assert_eq!(baf_iter.next().unwrap().data, baf6.data); + assert!(baf_iter.next().is_none()); + } + + #[test] + fn test_builder_closure() { + let mut buf = [0u32; 100]; + let (bar, mut tlv) = TlvBuilder::new::(&mut buf).unwrap(); + *bar = Bar { n: 5 }; + tlv.add_in::(|baz, tlv| { + *baz = Baz { n: 60 }; + *tlv.add::()? = Foo { + x: 20, + y: [1, 2, 3, 4], + }; + Ok(()) + }) + .unwrap(); + *tlv.add::().unwrap() = Foo { + x: 30, + y: [10, 20, 30, 40], + }; + let result_bytes = tlv.finish().into_bytes(&buf).unwrap(); + + let expected_bytes_explicit = &[ + // Bar Header and data (tag: "Bar_", len: 12 words, n: 5) + b'B', b'a', b'r', b'_', 0x0c, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, + // Baz Header and data (tag: "Baz_", len: 5 words, n: 60) + b'B', b'a', b'z', b'_', 0x05, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x00, + // Foo Header and data inside baz (tag: "FOO_", len: 2 words, x: 20, y: [1, 2, 3, 4]) + b'F', b'O', b'O', b'_', 0x02, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x01, 0x02, + 0x03, 0x04, + // Foo Header and data inside bar(tag: "FOO_", len: 2 words, x: 30, y: [10, 20, 30, 40]) + b'F', b'O', b'O', b'_', 0x02, 0x00, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x0a, 0x14, + 0x1e, 0x28, + ]; + assert_eq!(result_bytes, expected_bytes_explicit); + + // Overlay TlvData and parse the built container to verify round-trip correctness + let t = TlvData::overlay_bytes(result_bytes).unwrap(); + + let mut bar_iter = t.iter::(); + let bar_item = bar_iter.next().expect("Should have one Bar item"); + assert!(bar_iter.next().is_none()); + + assert_eq!(bar_item.container.header.tag, Bar::TAG); + assert_eq!(bar_item.container.header.word_len, 12); + assert_eq!(bar_item.data.n, 5); + + // Iterate over Bar's extensions + let mut ext_iter = bar_item.ext().iter::(); + + // First extension: Baz + let ext_baz = ext_iter.next().expect("Should have Baz extension"); + assert_eq!(ext_baz.container.header.tag, Baz::TAG); + assert_eq!(ext_baz.container.header.word_len, 5); + let baz_item = ext_baz.cast::().expect("Should cast to Baz"); + assert_eq!(baz_item.data.n, 60); + + { + // Baz's child: Foo (nested inside Baz) + let mut baz_ext_iter = baz_item.ext().iter::(); + let ext_foo_in_baz = baz_ext_iter.next().expect("Should have Foo inside Baz"); + assert_eq!(ext_foo_in_baz.container.header.tag, Foo::TAG); + assert_eq!(ext_foo_in_baz.container.header.word_len, 2); + let foo_in_baz = ext_foo_in_baz + .cast::() + .expect("Should cast to Foo inside Baz"); + assert_eq!(foo_in_baz.data.x, 20); + assert_eq!(foo_in_baz.data.y, [1, 2, 3, 4]); + assert!(baz_ext_iter.next().is_none()); + } + + // Second extension: Foo (flat child of Bar) + let ext_foo_in_bar = ext_iter.next().expect("Should have Foo inside Bar"); + assert_eq!(ext_foo_in_bar.container.header.tag, Foo::TAG); + assert_eq!(ext_foo_in_bar.container.header.word_len, 2); + let foo_in_bar = ext_foo_in_bar + .cast::() + .expect("Should cast to Foo inside Bar"); + assert_eq!(foo_in_bar.data.x, 30); + assert_eq!(foo_in_bar.data.y, [10, 20, 30, 40]); + assert!(ext_iter.next().is_none()); + } + + #[test] + fn test_builder_overflow() { + // Buffer big enough to hold the large TLVs. + // 70000 u32 words. + let mut buf = vec![0u32; 70000]; + let (bar, mut tlv) = TlvBuilder::new::(&mut buf).unwrap(); + *bar = Bar { n: 5 }; + + // We add 16382 Foo objects (4 words each = 65528 words) and 2 Baz objects + // (3 words each = 6 words total). + // This results in a child Baz payload of exactly 65535 words, which fits in u16. + // But the child total length (payload + 2 words header) is 65537 words, + // which overflows u16. + let res = tlv.add_in::(|baz, child_tlv| { + *baz = Baz { n: 42 }; + for _ in 0..16382 { + child_tlv.add::()?; + } + child_tlv.add::()?; + child_tlv.add::()?; + Ok(()) + }); + + // The finish_with_parent call must catch the u16 overflow and return Err(TlvError). + assert_eq!(res.unwrap_err(), TlvError); + } + + #[test] + fn test_cast_lifetime() { + let mut buf = [0u32; 16]; + let (foo, tlv) = TlvBuilder::new::(&mut buf).unwrap(); + *foo = Foo { x: 42, y: [0; 4] }; + let result_bytes = tlv.finish().into_bytes(&buf).unwrap(); + let data = TlvData::overlay_bytes(result_bytes).unwrap(); + + // Verify that we can cast and collect generic TLV items using standard + // iterator combinators. + let foos: Vec> = data + .iter::() + .filter_map(|item| item.cast::()) + .collect(); + + assert_eq!(foos.len(), 1); + assert_eq!(foos[0].data, &Foo { x: 42, y: [0; 4] }); + } + + #[test] + fn test_malformed_tlv_streams() { + // 1. Unaligned byte buffer passed to overlay_bytes must fail with BadAlignmentError + let aligned_buf = [0u8; 16]; + let unaligned_slice = &aligned_buf[1..13]; // Offset by 1 byte -> unaligned! + assert!(TlvData::overlay_bytes(unaligned_slice).is_err()); + + // 2. Buffer too short for a single header (header is 8 bytes = 2 words) + let too_short_words = [0u32; 1]; // Only 1 word (4 bytes) + let data = TlvData::overlay(&too_short_words); + assert!(data.iter::().next().is_none()); // Must fail gracefully, not panic! + + // 3. Header claims a payload length of 5 words, but buffer ends immediately after header + let header_only = [ + u32::from_le_bytes(*b"FOO_"), + 0x00000005, // word_len: 5, but no payload follows + ]; + let data = TlvData::overlay(&header_only); + assert!(data.iter::().next().is_none()); // Must fail gracefully (out-of-bounds) + + // 4. Header claims a payload length of 1 word, but the Foo struct requires at least 2 words + let header_too_small = [ + u32::from_le_bytes(*b"FOO_"), + 0x00000001, // word_len: 1, which is smaller than the Foo struct size (2 words)! + 0x00000014, // payload element 1 + ]; + let data = TlvData::overlay(&header_too_small); + assert!(data.iter::().next().is_none()); // Must fail gracefully (too small for struct) + + // 5. Completely empty buffer + let empty_words: [u32; 0] = []; + let data = TlvData::overlay(&empty_words); + assert!(data.iter::().next().is_none()); + } + + #[test] + fn test_iterator_underflow_skips() { + // Create a buffer containing: + // 1. A MiniFoo object (tag: FOO_, len: 1 word, x: 42) -- this represents an underflow for Foo + // 2. A Foo object (tag: FOO_, len: 2 words, x: 99, y: [9, 9, 9, 9]) -- a perfectly valid Foo + let buf = [ + // MiniFoo Header & Payload + u32::from_le_bytes(*b"FOO_"), + 0x00000001, // word_len: 1 + 42, // MiniFoo.x + // Foo Header & Payload + u32::from_le_bytes(*b"FOO_"), + 0x00000002, // word_len: 2 + 99, // Foo.x + u32::from_le_bytes([9, 9, 9, 9]), // Foo.y + ]; + + let data = TlvData::overlay(&buf); + + // We expect that iterating over Foo records will NOT abort on the first underflowed record. + // Even if the first record cannot be yielded as a full TlvItem<'_, Foo> (because it is too short), + // the iterator should advance past it and successfully yield the second, perfectly valid Foo record. + let mut iter = data.iter::(); + let second_item = iter + .next() + .expect("Should not terminate prematurely; should yield the second valid Foo"); + assert_eq!(second_item.data.x, 99); + assert_eq!(second_item.data.y, [9, 9, 9, 9]); + assert!(iter.next().is_none()); + } + + #[test] + fn test_casting_varied_sizes() { + let mut buf = [0u32; 16]; + + // ===================================================================== + // Scenario 1: Payload is 8 bytes (Foo) --- + // ===================================================================== + let (foo, tlv) = TlvBuilder::new::(&mut buf).unwrap(); + *foo = Foo { + x: 42, + y: [1, 2, 3, 4], + }; + let data = TlvData::overlay(tlv.finish().into_words(&buf).unwrap()); + let item = data.iter::().next().unwrap(); + + // --- Case 1A: Exact Match (Target: Foo) --- + + // `cast_zero_extended` succeeds and returns an owned, fully-populated struct. + let foo_cast_ze: Foo = item.cast_zero_extended::().unwrap(); + assert_eq!(foo_cast_ze.x, 42); + assert_eq!(foo_cast_ze.y, [1, 2, 3, 4]); + + // `cast` succeeds and returns a TlvItem referencing the struct with no extra extension. + let foo_cast: TlvItem<'_, Foo> = item.cast::().unwrap(); + assert_eq!(foo_cast.data.x, 42); + assert_eq!(foo_cast.data.y, [1, 2, 3, 4]); + assert!(foo_cast.extra.is_empty()); + + // `iter::()` succeeds and yields the exact match item with no extra extension. + let mut iter_exact = data.iter::(); + let item_exact = iter_exact.next().unwrap(); + assert_eq!(item_exact.data.x, 42); + assert_eq!(item_exact.data.y, [1, 2, 3, 4]); + assert!(item_exact.extra.is_empty()); + assert!(iter_exact.next().is_none()); + + // --- Case 1B: Shrunk Match / Overflow (Target: MiniFoo) --- + + // `cast_zero_extended` succeeds, returning an owned struct and discarding excess bytes. + let foo_shrunk_ze: MiniFoo = item.cast_zero_extended::().unwrap(); + assert_eq!(foo_shrunk_ze.x, 42); + + // `cast` succeeds, returning a TlvItem referencing MiniFoo, with the excess bytes placed in `extra`. + let foo_shrunk = item.cast::().unwrap(); + assert_eq!(foo_shrunk.data.x, 42); + assert_eq!(foo_shrunk.extra, &[u32::from_le_bytes([1, 2, 3, 4])]); + + // `iter::()` succeeds, yielding a TlvItem referencing MiniFoo, with the excess bytes placed in `extra`. + let mut iter_shrunk = data.iter::(); + let item_shrunk = iter_shrunk.next().unwrap(); + assert_eq!(item_shrunk.data.x, 42); + assert_eq!(item_shrunk.extra, &[u32::from_le_bytes([1, 2, 3, 4])]); + assert!(iter_shrunk.next().is_none()); + + // --- Case 1C: Mismatched Tag (Target: Parent) --- + + // All casting operations fail (return None) due to tag mismatch. + assert!(item.cast_zero_extended::().is_none()); + assert!(item.cast::().is_none()); + + // ===================================================================== + // Scenario 2: Payload is 4 bytes (MiniFoo) + // ===================================================================== + let (mini, tlv) = TlvBuilder::new::(&mut buf).unwrap(); + *mini = MiniFoo { x: 99 }; + let data = TlvData::overlay(tlv.finish().into_words(&buf).unwrap()); + let item = data.iter::().next().unwrap(); + + // --- Case 2A: Extended Match / Underflow (Target: Foo) --- + + // `cast_zero_extended` succeeds by zero-padding the missing fields of the target struct. + let foo_extended_ze: Foo = item.cast_zero_extended::().unwrap(); + assert_eq!(foo_extended_ze.x, 99); + assert_eq!(foo_extended_ze.y, [0, 0, 0, 0]); + + // `cast` fails (returns None) because the payload is smaller than the target struct. + assert!(item.cast::().is_none()); + + // `iter::()` skips the item (returns None) because the payload is smaller than the target struct. + let mut iter_extended = data.iter::(); + assert!(iter_extended.next().is_none()); + + // --- Case 2B: Mismatched Tag (Target: Parent) --- + + // All casting operations fail (return None) due to tag mismatch. + assert!(item.cast_zero_extended::().is_none()); + assert!(item.cast::().is_none()); + } + + #[test] + fn test_builder_with_slice() { + let mut buf_vec = vec![0u32; 32]; + let slice: &mut [u32] = &mut buf_vec; + + let (parent, mut builder) = TlvBuilder::new::(slice).unwrap(); + parent.id = 100; + + let child = builder.add::().unwrap(); + child.value = 200; + + // This should fail to compile before the fix: + let result_bytes = builder.finish().into_bytes(slice).unwrap(); + assert_eq!(result_bytes.len(), 24); + + let data = TlvData::overlay_bytes(result_bytes).unwrap(); + let parent_item = data.iter::().next().unwrap(); + assert_eq!(parent_item.data.id, 100); + let child_item = parent_item.ext().iter::().next().unwrap(); + assert_eq!(child_item.data.value, 200); + } + + tlv_struct! { FooV1, *b"FOO_", [u8], + #[derive(Debug, PartialEq, Eq, Default)] + pub struct FooV1 { + pub a: u32, + pub b: u32, + } + } + + tlv_struct! { FooV2, *b"FOO_", [u8], + #[derive(Debug, PartialEq, Eq, Default)] + pub struct FooV2 { + pub a: u32, + pub b: u32, + pub c: u32, + } + } + + #[test] + fn test_forward_compatibility() { + let mut buf = [0u32; 32]; + + // V2 firmware writes FooV2 + let (foo_v2, tlv) = TlvBuilder::new::(&mut buf).unwrap(); + foo_v2.a = 100; + foo_v2.b = 200; + foo_v2.c = 300; + let result_bytes = tlv.finish().into_bytes(&buf).unwrap(); + + // V1 firmware reads it as FooV1 + let data = TlvData::overlay_bytes(result_bytes).unwrap(); + let mut iter = data.iter::(); + let item = iter.next().expect("Should be able to parse as FooV1"); + assert!(iter.next().is_none()); + + assert_eq!(item.data.a, 100); + assert_eq!(item.data.b, 200); + + // The extra field 'c' (300) should be in the extension bytes + let ext_bytes = item.ext(); + assert_eq!(ext_bytes.len(), 4); + let c_val = u32::from_le_bytes(ext_bytes.try_into().unwrap()); + assert_eq!(c_val, 300); + } + + #[test] + fn test_backward_compatibility_fails_directly() { + let mut buf = [0u32; 32]; + + // V1 firmware writes FooV1 (2 words) + let (foo_v1, tlv) = TlvBuilder::new::(&mut buf).unwrap(); + foo_v1.a = 100; + foo_v1.b = 200; + let result_bytes = tlv.finish().into_bytes(&buf).unwrap(); + + // V2 firmware tries to read it as FooV2 (expects 3 words) + let data = TlvData::overlay_bytes(result_bytes).unwrap(); + let mut iter = data.iter::(); + + // This returns None because the payload is too short for FooV2 + assert!(iter.next().is_none()); + } + + #[test] + fn test_forward_compatibility_padded() { + let mut buf = [0u32; 32]; + + // V1 firmware writes FooV1 (2 words) + let (foo_v1, tlv) = TlvBuilder::new::(&mut buf).unwrap(); + foo_v1.a = 100; + foo_v1.b = 200; + let result_bytes = tlv.finish().into_bytes(&buf).unwrap(); + + // V2 firmware reads it as TlvAny first, then uses cast_zero_extended + let data = TlvData::overlay_bytes(result_bytes).unwrap(); + let mut iter = data.iter::(); + let item = iter.next().expect("Should find the TLV"); + assert!(iter.next().is_none()); + + // Read as FooV2, which should succeed and 0-initialize 'c' + let foo_v2 = item + .cast_zero_extended::() + .expect("Should read as FooV2"); + assert_eq!(foo_v2.a, 100); + assert_eq!(foo_v2.b, 200); + assert_eq!(foo_v2.c, 0); // 'c' is 0-initialized + } + + #[test] + fn test_builder_add_copy() { + // First build a Baz with a nested Foo child and some ext data + let mut src_buf = [0u32; 64]; + let (baz, mut baz_builder) = TlvBuilder::new::(&mut src_buf).unwrap(); + baz.n = 42; + baz_builder.add_with_ext::(&[10, 20, 30]).unwrap(); + let src_bytes = baz_builder.finish().into_bytes(&src_buf).unwrap(); + + // Parse it back as a TlvItem + let src_data = TlvData::overlay_bytes(src_bytes).unwrap(); + let baz_item = src_data.iter::().next().unwrap(); + + // Now build a Parent container and add a copy of Baz (with its Foo child) into it + let mut dest_buf = [0u32; 64]; + let (parent, mut parent_builder) = TlvBuilder::new::(&mut dest_buf).unwrap(); + parent.id = 1; + + parent_builder.add_copy(&baz_item).unwrap(); + + // Also add another child to parent to ensure builder state remains valid + let extra_child = parent_builder.add::().unwrap(); + extra_child.value = 99; + + let dest_bytes = parent_builder.finish().into_bytes(&dest_buf).unwrap(); + + // Parse back the destination container and verify everything + let dest_data = TlvData::overlay_bytes(dest_bytes).unwrap(); + let parent_item = dest_data.iter::().next().unwrap(); + // The copy landed after the Parent struct, leaving it untouched. + assert_eq!(parent_item.data.id, 1); + + let mut children = parent_item.ext().iter::(); + // The copy is byte-identical to the source container, padding flags and all. + assert_eq!(children.next().unwrap().container.as_bytes(), src_bytes); + // The sibling added after the copy is intact, so the builder state stayed consistent. + assert_eq!( + children.next().unwrap().cast::().unwrap().data.value, + 99 + ); + assert!(children.next().is_none()); + } + + #[test] + fn test_builder_add_copy_overflow() { + let mut src_buf = [0u32; 32]; + let (child, builder) = TlvBuilder::new::(&mut src_buf).unwrap(); + child.value = 123; + let src_bytes = builder.finish().into_bytes(&src_buf).unwrap(); + + let src_data = TlvData::overlay_bytes(src_bytes).unwrap(); + let child_item = src_data.iter::().next().unwrap(); + + // Destination buffer has one spare word after the Parent header and struct (3 words), + // not enough for copying the Child (which needs 2 header + 1 data = 3 words). + let mut small_buf = [0u32; 4]; + let (_parent, mut parent_builder) = TlvBuilder::new::(&mut small_buf).unwrap(); + assert_eq!(parent_builder.add_copy(&child_item), Err(TlvError)); + + // The failed add_copy spoiled the builder, so finishing it won't yield a buffer. + assert_eq!( + parent_builder.finish().into_bytes(&small_buf), + Err(InvalidBufferError) + ); + } +} diff --git a/shared/tlv/options.proto b/shared/tlv/options.proto new file mode 100644 index 00000000..faa47058 --- /dev/null +++ b/shared/tlv/options.proto @@ -0,0 +1,53 @@ +// Copyright 2026 Google LLC +// +// 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. + +syntax = "proto3"; + +package openprot.tlv; + +import "google/protobuf/descriptor.proto"; + +extend google.protobuf.FieldOptions { + // The byte offset to a particular field in the zerocopy struct that + // represents the fixed fields. Must be naturally aligned. + uint32 offset = 12345; + + // The packed Rust type of the field in the fixed struct. E.g., "u32", "u8", + // "[u8; 16]", or "[u32; 4]". If omitted, defaults to the size/type of the + // field's Protobuf type. + string type = 12346; + + // The bit range of a field in a bit-packed message using Verilog notation. + // Can be a multi-bit range (e.g., "7:3") or a single bit (e.g., "0"). + string bit_range = 12347; + + // Indicates that a string or bytes field is stored as the unpadded + // variable-length extra data following the fixed payload fields of the TLV + // record. + bool extra_data = 12348; +} + +extend google.protobuf.MessageOptions { + string tlv_fourcc = 12345; + + // The backing integer type of a bit-packed message ("u8", "u16", or "u32"). + string bit_packed = 12346; +} + +extend google.protobuf.EnumOptions { + // The backing primitive integer type for this enum (e.g. "u8", "u16", "i32", + // "u32"). If present, this enum is generated as a zero-copy open enum + // wrapping this type. + string raw_type = 12345; +} diff --git a/shared/tlv/proto_conv.rs b/shared/tlv/proto_conv.rs new file mode 100644 index 00000000..dacdd11b --- /dev/null +++ b/shared/tlv/proto_conv.rs @@ -0,0 +1,1949 @@ +// Copyright 2026 Google LLC +// +// 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. + +use openprot_proto_reader::FieldType; +use openprot_proto_reader::ProtoInputError; +use openprot_proto_reader::ProtoReader; +use openprot_proto_reader::Record; +use openprot_proto_reflect::TypeExt; +use openprot_tlv::TlvFlags; +use openprot_tlv::TlvHeader; +use openprot_tlv_proto_meta::DescriptorProtoExt; +use openprot_tlv_proto_meta::DescriptorProtoTlvExt; +use openprot_tlv_proto_meta::FieldLayout; +use openprot_tlv_proto_meta::NameStrExt; +use openprot_tlv_proto_meta::TlvLayout; +use openprot_tlv_proto_meta::TlvLayoutError; +use openprot_tlv_proto_meta::Type; +use openprot_tlv_proto_meta::find_message_descriptor; +use std::fmt; +use zerocopy::IntoBytes; + +// Re-export the descriptor rust proto so clients can pass it +pub use descriptor_rust_proto::FileDescriptorSet; + +// Re-export DescriptorInfo so clients can easily type it +pub use ::protobuf::__internal::runtime::__unstable::DescriptorInfo; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ProtoConvError { + MessageNotFound(String), + MissingTlvFourcc(String), + InvalidTlvFourcc(String), + VarintDecodeError, + UnsupportedWireType(u8), + InvalidUtf8, + ProtoInput(ProtoInputError), + InvalidLayout(Box), + FieldValueTooLong { + field_name: String, + value_len: usize, + max_len: usize, + }, + FieldValueOutOfRange { + field_name: String, + value: u64, + bit_len: usize, + is_signed: bool, + }, + ByteFieldLengthMismatch { + field_name: String, + actual_len: usize, + expected_len: usize, + }, +} + +impl From for ProtoConvError { + fn from(err: ProtoInputError) -> Self { + Self::ProtoInput(err) + } +} + +impl From for ProtoConvError { + fn from(err: TlvLayoutError) -> Self { + Self::InvalidLayout(Box::new(err)) + } +} + +impl fmt::Display for ProtoConvError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::MessageNotFound(msg) => write!(f, "Message descriptor not found: {}", msg), + Self::MissingTlvFourcc(msg) => { + write!(f, "Missing tlv_fourcc option on message: {}", msg) + } + Self::InvalidTlvFourcc(err) => write!(f, "Invalid tlv_fourcc option: {}", err), + Self::VarintDecodeError => { + write!(f, "Failed to decode Varint from protobuf wire format") + } + Self::UnsupportedWireType(wt) => write!(f, "Unsupported protobuf wire type: {}", wt), + Self::InvalidUtf8 => write!(f, "Invalid UTF-8 in descriptor string"), + Self::ProtoInput(err) => write!(f, "Protobuf stream read error: {}", err), + Self::InvalidLayout(err) => write!(f, "{}", err), + Self::FieldValueTooLong { + field_name, + value_len, + max_len, + } => { + write!( + f, + "Value for field '{}' is too long (length is {} bytes, but target packed size in the fixed struct is only {} bytes).", + field_name, value_len, max_len + ) + } + Self::FieldValueOutOfRange { + field_name, + value, + bit_len, + is_signed, + } => { + let bit_len = *bit_len; + if *is_signed { + let min_value = if bit_len >= 64 { + i64::MIN + } else { + -(1i64 << (bit_len - 1)) + }; + let max_value = if bit_len >= 64 { + i64::MAX + } else { + (1i64 << (bit_len - 1)) - 1 + }; + write!( + f, + "Value {} for field '{}' is out of range for its packed bit-width of {} bits (valid range is {}..={}).", + *value as i64, field_name, bit_len, min_value, max_value + ) + } else { + let max_value = if bit_len >= 64 { + u64::MAX + } else { + (1u64 << bit_len) - 1 + }; + write!( + f, + "Value {} for field '{}' is out of range for its packed bit-width of {} bits (valid range is 0..={}).", + value, field_name, bit_len, max_value + ) + } + } + Self::ByteFieldLengthMismatch { + field_name, + actual_len, + expected_len, + } => { + write!( + f, + "Length of bytes field '{}' must match the target size exactly (expected {} bytes, got {} bytes).", + field_name, expected_len, actual_len + ) + } + } + } +} + +impl std::error::Error for ProtoConvError {} + +/// Encodes a serialized protobuf message into its packed TLV representation. +/// +/// Returns a guaranteed 32-bit aligned `Vec` buffer representing the TLV data. +pub fn proto_to_tlv( + desc_set: &FileDescriptorSet, + message_name: &str, + proto_bytes: &[u8], +) -> Result, ProtoConvError> { + let msg_desc = find_message_descriptor(desc_set, message_name) + .ok_or_else(|| ProtoConvError::MessageNotFound(message_name.to_string()))?; + + let mut out = Vec::new(); + encode_message(desc_set, msg_desc, proto_bytes, &mut out)?; + Ok(out) +} + +/// Encodes a protobuf message struct directly into its packed TLV representation, +/// using the self-contained `DescriptorInfo` generated by the protobuf compiler. +/// +/// Returns a guaranteed 32-bit aligned `Vec` buffer representing the TLV data. +pub fn message_to_tlv( + info: &DescriptorInfo, + message: &M, +) -> Result, ProtoConvError> { + let full_type_name = std::any::type_name::(); + + let proto_bytes = message + .serialize() + .map_err(|_| ProtoConvError::VarintDecodeError)?; + + proto_to_tlv_with_info(info, full_type_name, &proto_bytes) +} + +/// Encodes a serialized protobuf message into its packed TLV representation, +/// using the self-contained `DescriptorInfo` generated by the protobuf compiler. +/// +/// Returns a guaranteed 32-bit aligned `Vec` buffer representing the TLV data. +pub fn proto_to_tlv_with_info( + info: &DescriptorInfo, + message_name: &str, + proto_bytes: &[u8], +) -> Result, ProtoConvError> { + let desc_set = openprot_tlv_proto_meta::collect_descriptor_set(info) + .ok_or(ProtoConvError::VarintDecodeError)?; + + proto_to_tlv(&desc_set, message_name, proto_bytes) +} + +/// Encodes a protobuf message into its packed TLV representation, including the 8-byte TLV header. +fn encode_message<'a>( + desc_set: &'a FileDescriptorSet, + msg_desc: descriptor_rust_proto::DescriptorProtoView<'a>, + proto_bytes: &[u8], + out: &mut Vec, +) -> Result<(), ProtoConvError> { + let tag = msg_desc + .tlv_fourcc() + .ok_or_else(|| ProtoConvError::MissingTlvFourcc(msg_desc.name_str().to_string()))?; + + let header_len = core::mem::size_of::(); + let header_start_bytes = out.len() * 4; + + // Reserve space for the header using TlvHeader directly (2 words) + out.resize(out.len() + 2, 0); + + let body_end_bytes = encode_message_body( + desc_set, + msg_desc, + proto_bytes, + out, + header_start_bytes + header_len, + )?; + + let raw_body_len = body_end_bytes - header_start_bytes - header_len; + let padded_body_len = (raw_body_len + 3) & !3; + let padding_bytes = (padded_body_len - raw_body_len) as u8; + + if padding_bytes > 0 { + out.resize((header_start_bytes + header_len + padded_body_len) / 4, 0); + } + + let word_len = + u16::try_from(padded_body_len / 4).map_err(|_| ProtoConvError::FieldValueTooLong { + field_name: msg_desc.name_str().to_string(), + value_len: padded_body_len, + max_len: 65535 * 4, + })?; + + let header = TlvHeader { + tag: u32::from_le_bytes(tag), + word_len, + reserved: 0, + flags: TlvFlags::new().with_padding_bytes(padding_bytes), + }; + + // Cast the Vec to a mutable byte slice using zerocopy and write the header + let out_bytes = out.as_mut_bytes(); + out_bytes[header_start_bytes..header_start_bytes + header_len] + .copy_from_slice(header.as_bytes()); + + Ok(()) +} + +/// Encodes only the raw body of a message (fixed struct and dynamic TLVs), without any TLV header. +/// Returns the exact end position in bytes of the unpadded body payload. +fn encode_message_body<'a>( + desc_set: &'a FileDescriptorSet, + msg_desc: descriptor_rust_proto::DescriptorProtoView<'a>, + proto_bytes: &[u8], + out: &mut Vec, + body_start: usize, +) -> Result { + // 1. Resolve and validate the entire message layout upfront + let layout = msg_desc.tlv_layout(desc_set)?; + + // 2. Write placeholder for the message body (grow only if needed) + let fixed_struct_end = body_start + layout.fixed_struct_size(); + let required_words = fixed_struct_end.div_ceil(4); + if required_words > out.len() { + out.resize(required_words, 0); + } + let mut exact_body_end = fixed_struct_end; + + // 3. Parse and encode the message based on its layout strategy! + match layout { + TlvLayout::Standard { + fields, + fixed_struct_size: _, + } => { + let mut reader = ProtoReader::new(proto_bytes); + while !reader.is_eof() { + let (field_number, record) = reader.read_record()?; + + let field_layout = match fields.get(&field_number) { + Some(l) => l, + None => continue, + }; + + let Some(field_desc) = msg_desc.find_field(field_number) else { + continue; + }; + + match field_layout { + FieldLayout::Fixed { + offset, + packed_type, + } => { + let size = packed_type.size(); + let val_pos = body_start + offset; + let required_words = (val_pos + size).div_ceil(4); + if required_words <= out.len() { + if let Record::LengthDelimited(src_bytes) = record { + // If the field is a nested message, recursively encode its body inline! + if field_desc.r#type() == Type::Message + || field_desc.r#type() == Type::Group + { + let type_name_raw = field_desc + .type_name() + .to_str() + .map_err(|_| ProtoConvError::InvalidUtf8)?; + let type_name = + type_name_raw.strip_prefix('.').unwrap_or(type_name_raw); + + let child_desc = find_message_descriptor(desc_set, type_name) + .ok_or_else(|| { + ProtoConvError::MessageNotFound(type_name.to_string()) + })?; + + // Recursively encode child body directly in place inside parent's buffer! + encode_message_body( + desc_set, child_desc, src_bytes, out, val_pos, + )?; + } else { + // Standard inline string or bytes + if field_desc.r#type() == Type::Bytes { + if src_bytes.len() != size { + return Err(ProtoConvError::ByteFieldLengthMismatch { + field_name: field_desc.name_str().to_string(), + actual_len: src_bytes.len(), + expected_len: size, + }); + } + } else if src_bytes.len() > size { + return Err(ProtoConvError::FieldValueTooLong { + field_name: field_desc.name_str().to_string(), + value_len: src_bytes.len(), + max_len: size, + }); + } + let copy_len = src_bytes.len(); + + // Cast the Vec to mutable byte slice and copy + let out_bytes = out.as_mut_bytes(); + out_bytes[val_pos..val_pos + copy_len] + .copy_from_slice(&src_bytes[..copy_len]); + if copy_len < size { + out_bytes[val_pos + copy_len..val_pos + size].fill(0); + } + } + } else { + let val = record + .decode_u64(FieldType(i32::from(field_desc.r#type()))) + .map_err(|_| { + ProtoConvError::UnsupportedWireType(record.wire_type_u8()) + })?; + + let field_name = field_desc.name_str(); + if !packed_type.is_float() { + validate_value_range( + val, + packed_type.is_signed(), + size * 8, + field_name, + )?; + } + + let out_bytes = out.as_mut_bytes(); + match size { + 1 => out_bytes[val_pos] = val as u8, + 2 => out_bytes[val_pos..val_pos + 2] + .copy_from_slice(&(val as u16).to_le_bytes()), + 4 => out_bytes[val_pos..val_pos + 4] + .copy_from_slice(&(val as u32).to_le_bytes()), + 8 => out_bytes[val_pos..val_pos + 8] + .copy_from_slice(&val.to_le_bytes()), + _ => return Err(ProtoConvError::VarintDecodeError), + } + } + } + } + FieldLayout::Tlv { .. } => { + let child_bytes = match record { + Record::LengthDelimited(bytes) => bytes, + _ => { + return Err(ProtoConvError::UnsupportedWireType( + record.wire_type_u8(), + )); + } + }; + + let type_name_raw = field_desc + .type_name() + .to_str() + .map_err(|_| ProtoConvError::InvalidUtf8)?; + let type_name = type_name_raw.strip_prefix('.').unwrap_or(type_name_raw); + + let child_desc = + find_message_descriptor(desc_set, type_name).ok_or_else(|| { + ProtoConvError::MessageNotFound(type_name.to_string()) + })?; + + // Recursive call to encode_message (since it's a dynamic sub-TLV, it must have a header!) + encode_message(desc_set, child_desc, child_bytes, out)?; + exact_body_end = out.len() * 4; + } + FieldLayout::ExtraData => { + let child_bytes = match record { + Record::LengthDelimited(bytes) => bytes, + _ => { + return Err(ProtoConvError::UnsupportedWireType( + record.wire_type_u8(), + )); + } + }; + let start_pos = fixed_struct_end; + let src_len = child_bytes.len(); + let target_end = start_pos + .checked_add(src_len) + .ok_or(ProtoConvError::VarintDecodeError)?; + let target_words = target_end.div_ceil(4); + out.truncate(target_words); + if target_words > out.len() { + out.resize(target_words, 0); + } + let out_bytes = out.as_mut_bytes(); + out_bytes[start_pos..start_pos + src_len].copy_from_slice(child_bytes); + if target_end < target_words * 4 { + out_bytes[target_end..target_words * 4].fill(0); + } + exact_body_end = target_end; + } + } + } + } + TlvLayout::BitPacked { fields, backing } => { + // --- 2. Bit-Packed Message Serialization --- + let total_size = backing.size(); + let mut accum = 0u64; + + // Gather, validate, and pack all field values in a single pass over the protobuf stream! + let mut reader = ProtoReader::new(proto_bytes); + while !reader.is_eof() { + let (field_number, record) = reader.read_record()?; + let Some(bit_field) = fields.get(&field_number) else { + continue; + }; + let Some(fd) = msg_desc.find_field(field_number) else { + continue; + }; + let val = record + .decode_u64(FieldType(i32::from(fd.r#type()))) + .map_err(|_| ProtoConvError::UnsupportedWireType(record.wire_type_u8()))?; + + let field_name = fd.name_str(); + let is_signed = fd + .r#type() + .primitive_info() + .is_some_and(|info| info.is_signed); + validate_value_range(val, is_signed, bit_field.bit_len, field_name)?; + + // Clear any previously written bits for this field to ensure standard overriding behavior + let mask = mask_for_bits(bit_field.bit_len); + accum &= !(mask << bit_field.bit_offset); + accum |= (val & mask) << bit_field.bit_offset; + } + + // Write the accumulated bytes directly into the output buffer + let backing_bytes = accum.to_le_bytes(); + let out_bytes = out.as_mut_bytes(); + out_bytes[body_start..body_start + total_size] + .copy_from_slice(&backing_bytes[..total_size]); + exact_body_end = body_start + total_size; + } + } + Ok(exact_body_end) +} + +fn mask_for_bits(bit_len: usize) -> u64 { + if bit_len >= 64 { + u64::MAX + } else { + (1u64 << bit_len) - 1 + } +} + +/// Sign-extends a `val` of width `bit_len` to a 64-bit signed integer represented as `u64`. +/// +/// `bit_len` must be between 1 and 64 inclusive. +fn sign_extend(val: u64, bit_len: usize) -> u64 { + if bit_len == 0 || bit_len >= 64 { + val + } else { + let shift = 64 - bit_len; + ((val << shift) as i64 >> shift) as u64 + } +} + +fn validate_value_range( + val: u64, + is_signed: bool, + bit_len: usize, + field_name: &str, +) -> Result<(), ProtoConvError> { + let fits = if bit_len >= 64 { + true + } else if is_signed { + sign_extend(val, bit_len) == val + } else { + (val >> bit_len) == 0 + }; + + if !fits { + return Err(ProtoConvError::FieldValueOutOfRange { + field_name: field_name.to_string(), + value: val, + bit_len, + is_signed, + }); + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + use openprot_tlv::TlvData; + use protobuf::Serialize; + + use edge_cases_rust_proto::__unstable::SHARED_TLV_TEST_EDGE_CASES_DESCRIPTOR_INFO; + use soc_rust_proto::__unstable::SHARED_TLV_TEST_SOC_DESCRIPTOR_INFO; + + #[test] + fn test_sign_extend() { + // 8-bit sign extension + assert_eq!(sign_extend(0x7F, 8), 0x7F); // 127 + assert_eq!(sign_extend(0x80, 8), 0xFFFFFFFFFFFFFF80); // -128 + assert_eq!(sign_extend(0xFF, 8), 0xFFFFFFFFFFFFFFFF); // -1 + + // 16-bit sign extension + assert_eq!(sign_extend(0x7FFF, 16), 0x7FFF); // 32767 + assert_eq!(sign_extend(0x8000, 16), 0xFFFFFFFFFFFF8000); // -32768 + + // 32-bit sign extension + assert_eq!(sign_extend(0x7FFFFFFF, 32), 0x7FFFFFFF); + assert_eq!(sign_extend(0x80000000, 32), 0xFFFFFFFF80000000); + assert_eq!(sign_extend(0xFFFFFFFB, 32), 0xFFFFFFFFFFFFFFFB); // -5 + + // Arbitrary width (e.g. 11-bit) + assert_eq!(sign_extend(0x3FF, 11), 0x3FF); // 1023 + assert_eq!(sign_extend(0x400, 11), 0xFFFFFFFFFFFFFC00); // -1024 + assert_eq!(sign_extend(0x79C, 11), 0xFFFFFFFFFFFFFF9C); // -100 + + // 64-bit (no-op) + assert_eq!(sign_extend(0xFFFFFFFFFFFFFFFB, 64), 0xFFFFFFFFFFFFFFFB); + } + + #[test] + fn test_proto_to_tlv_system_hierarchy() { + // 1. Build the nested topology containing all three accelerators using the proto! macro + let system = protobuf::proto!(soc_rust_proto::System { + vendor_id: 32898, + product_id: 4660, + total_mem_mib: 65536, + system_name: "MyAwesomeSystem".to_string(), + system_uuid: vec![ + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, + 0x0F, 0x10, + ], + cores: [soc_rust_proto::CpuCore { + base_frequency_mhz: 3600, + id: soc_rust_proto::CoreIdentifier { + model_id: 137, + revision: 3, + }, + cache: soc_rust_proto::CacheInfo { + level: 1, + size_kib: 64, + associativity: 8, + }, + execution_units: [ + soc_rust_proto::ExecutionUnit { + unit_type: 0, + pipeline_stages: 5, + state: soc_rust_proto::ExecutionUnitState::EuStateLarge, + power_mw: 350, + }, + soc_rust_proto::ExecutionUnit { + unit_type: 1, + pipeline_stages: 8, + state: soc_rust_proto::ExecutionUnitState::EuStateIdle, + power_mw: 50, + }, + ], + }], + bus: soc_rust_proto::SystemBus { + bus_type: 1, + width_bits: 64, + speed_mhz: 3200, + features: soc_rust_proto::BusFeatures { + is_primary: true, + supports_dma: false, + supports_coherent: true, + burst_len_log2: 4, + }, + }, + accelerators: [ + protobuf::proto!(soc_rust_proto::Accelerator { + gpu: soc_rust_proto::GpuAccelerator { + cores: 512, + frequency_mhz: 1200, + } + }), + protobuf::proto!(soc_rust_proto::Accelerator { + npu: soc_rust_proto::NpuAccelerator { tops: 42 } + }), + protobuf::proto!(soc_rust_proto::Accelerator { + crypto: soc_rust_proto::CryptoAccelerator { + key_size_bits: 256, + supported_ciphers: b"AES,RSA".to_vec(), + } + }), + ], + }); + + // 2. Convert to TLV using our library's message_to_tlv function! + let tlv_words = message_to_tlv(&SHARED_TLV_TEST_SOC_DESCRIPTOR_INFO, &system) + .expect("Failed to convert proto to TLV!"); + + // 3. Verify the exact expected byte representation + let mut expected = Vec::new(); + + // --- System Header --- + expected.extend_from_slice(b"SYS_"); // tag + expected.extend_from_slice(&[0x35, 0x00, 0x00, 0x00]); // word_len: 53, reserved: 0, flags: 0 + + // --- System Fixed Struct (44 bytes) --- + expected.extend_from_slice(&32898u32.to_le_bytes()); // vendor_id (offset 0) + expected.extend_from_slice(&4660u32.to_le_bytes()); // product_id (offset 4) + expected.extend_from_slice(&65536u32.to_le_bytes()); // total_mem_mib (offset 8) + + // system_name (offset 12, 16 bytes: "MyAwesomeSystem" + 1 null padding byte) + expected.extend_from_slice(b"MyAwesomeSystem\0"); + + // system_uuid (offset 28, 16 bytes) + expected.extend_from_slice(&[ + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, + 0x0F, 0x10, + ]); + + // --- CpuCore Sub-TLV (72 bytes) --- + expected.extend_from_slice(b"CORE"); // tag + expected.extend_from_slice(&[0x10, 0x00, 0x00, 0x00]); // word_len: 16, reserved: 0, flags: 0 + expected.extend_from_slice(&3600u32.to_le_bytes()); // base_frequency_mhz (offset 0) + expected.extend_from_slice(&137u32.to_le_bytes()); // id.model_id (offset 4) + expected.extend_from_slice(&3u32.to_le_bytes()); // id.revision (offset 8) + + // CpuCore -> CacheInfo Sub-TLV (20 bytes) + expected.extend_from_slice(b"CACH"); // tag + expected.extend_from_slice(&[0x03, 0x00, 0x00, 0x00]); // word_len: 3, reserved: 0, flags: 0 + expected.extend_from_slice(&1u32.to_le_bytes()); // level + expected.extend_from_slice(&64u32.to_le_bytes()); // size_kib + expected.extend_from_slice(&8u32.to_le_bytes()); // associativity + + // CpuCore -> First ExecutionUnit Sub-TLV (16 bytes) + expected.extend_from_slice(b"EXEU"); // tag + expected.extend_from_slice(&[0x02, 0x00, 0x00, 0x00]); // word_len: 2, reserved: 0, flags: 0 + expected.push(0); // unit_type (offset 0, 1 byte) + expected.push(5); // pipeline_stages (offset 1, 1 byte) + expected.push(150); // state (offset 2, 1 byte) + expected.push(0); // padding (offset 3, 1 byte) + expected.extend_from_slice(&350u16.to_le_bytes()); // power_mw (offset 4, 2 bytes) -> [94, 1] + expected.extend_from_slice(&[0u8; 2]); // body padding to 4 bytes + + // CpuCore -> Second ExecutionUnit Sub-TLV (16 bytes) + expected.extend_from_slice(b"EXEU"); // tag + expected.extend_from_slice(&[0x02, 0x00, 0x00, 0x00]); // word_len: 2, reserved: 0, flags: 0 + expected.push(1); // unit_type (offset 0, 1 byte) + expected.push(8); // pipeline_stages (offset 1, 1 byte) + expected.push(0); // state (offset 2, 1 byte) + expected.push(0); // padding (offset 3, 1 byte) + expected.extend_from_slice(&50u16.to_le_bytes()); // power_mw (offset 4, 2 bytes) -> [50, 0] + expected.extend_from_slice(&[0u8; 2]); // body padding to 4 bytes + + // --- SystemBus Sub-TLV (24 bytes) --- + expected.extend_from_slice(b"SBUS"); // tag + expected.extend_from_slice(&[0x04, 0x00, 0x00, 0x00]); // word_len: 4, reserved: 0, flags: 0 + expected.extend_from_slice(&1u32.to_le_bytes()); // bus_type + expected.extend_from_slice(&64u32.to_le_bytes()); // width_bits + expected.extend_from_slice(&3200u32.to_le_bytes()); // speed_mhz + expected.push(37); // features (offset 12, 1 byte) + expected.extend_from_slice(&[0u8; 3]); // body padding to 4 bytes + + // --- First Accelerator Sub-TLV: GPU (24 bytes) --- + expected.extend_from_slice(b"ACCL"); // tag + expected.extend_from_slice(&[0x04, 0x00, 0x00, 0x00]); // word_len: 4, reserved: 0, flags: 0 + expected.extend_from_slice(b"GPU_"); // tag + expected.extend_from_slice(&[0x02, 0x00, 0x00, 0x00]); // word_len: 2, reserved: 0, flags: 0 + expected.extend_from_slice(&512u32.to_le_bytes()); // cores + expected.extend_from_slice(&1200u32.to_le_bytes()); // frequency_mhz + + // --- Second Accelerator Sub-TLV: NPU (20 bytes) --- + expected.extend_from_slice(b"ACCL"); // tag + expected.extend_from_slice(&[0x03, 0x00, 0x00, 0x00]); // word_len: 3, reserved: 0, flags: 0 + expected.extend_from_slice(b"NPU_"); // tag + expected.extend_from_slice(&[0x01, 0x00, 0x00, 0x00]); // word_len: 1, reserved: 0, flags: 0 + expected.extend_from_slice(&42u32.to_le_bytes()); // tops + + // --- Third Accelerator Sub-TLV: Crypto (28 bytes) --- + expected.extend_from_slice(b"ACCL"); // tag + expected.extend_from_slice(&[0x05, 0x00, 0x00, 0x00]); // word_len: 5, reserved: 0, flags: 0 + expected.extend_from_slice(b"CRYP"); // tag + expected.extend_from_slice(&[0x03, 0x00, 0x00, 0x01]); // word_len: 3, reserved: 0, flags: 1 + expected.extend_from_slice(&256u32.to_le_bytes()); // key_size_bits + expected.extend_from_slice(b"AES,RSA\0"); // supported_ciphers + + assert_eq!(tlv_words.as_bytes(), expected); + + // 4. Verify the output by parsing it using the generated zero-copy Rust structures! + use openprot_tlv::TlvObject; + use soc_tlv::*; + + let tlv_data = openprot_tlv::TlvData::overlay(&tlv_words); + + // Verify there is exactly one top-level TLV and it is System + let mut top_any_iter = tlv_data.iter::(); + let top_item = top_any_iter.next().unwrap(); + assert_eq!(top_item.container.header.tag, System::TAG); + assert!( + top_any_iter.next().is_none(), + "Expected no other top-level TLVs" + ); + + // Assert System fields + let system_tlv = tlv_data.iter::().next().unwrap(); + assert_eq!(system_tlv.data.vendor_id, 32898); + assert_eq!(system_tlv.data.product_id, 4660); + assert_eq!(system_tlv.data.total_mem_mib, 65536); + assert_eq!(&system_tlv.data.system_name[..15], b"MyAwesomeSystem"); + assert_eq!(system_tlv.data.system_name[15], 0); + assert_eq!( + system_tlv.data.system_uuid, + [0x04030201, 0x08070605, 0x0C0B0A09, 0x100F0E0D] + ); + + // Assert Nested CpuCore via generated getter, and ensure iterator is exhausted + let mut cores_iter = system_tlv.cores(); + let core = cores_iter.next().unwrap(); + assert_eq!(core.data.base_frequency_mhz, 3600); + assert_eq!(core.data.id.model_id, 137); + assert_eq!(core.data.id.revision, 3); + assert!(cores_iter.next().is_none(), "Expected exactly one CpuCore"); + + // Assert Nested CacheInfo inside CpuCore via generated getter + let cache = core.cache().unwrap(); + assert_eq!(cache.data.level, 1); + assert_eq!(cache.data.size_kib, 64); + assert_eq!(cache.data.associativity, 8); + + // Assert Nested ExecutionUnits inside CpuCore via generated getter, and ensure iterator is exhausted + let mut eu_iter = core.execution_units(); + + let eu1 = eu_iter.next().unwrap(); + assert_eq!(eu1.data.unit_type, 0); + assert_eq!(eu1.data.pipeline_stages, 5); + assert_eq!(eu1.data.state, ExecutionUnitState::LARGE); // EuStateLarge + assert_eq!(eu1.data.power_mw, 350); + + let eu2 = eu_iter.next().unwrap(); + assert_eq!(eu2.data.unit_type, 1); + assert_eq!(eu2.data.pipeline_stages, 8); + assert_eq!(eu2.data.state, ExecutionUnitState::IDLE); // EuStateIdle + assert_eq!(eu2.data.power_mw, 50); + + assert!( + eu_iter.next().is_none(), + "Expected exactly two ExecutionUnits" + ); + + // Assert Nested SystemBus via generated getter + let bus = system_tlv.bus().unwrap(); + assert_eq!(bus.data.bus_type, 1); + assert_eq!(bus.data.width_bits, 64); + assert_eq!(bus.data.speed_mhz, 3200); + assert!(bus.data.features.is_primary()); + assert!(!bus.data.features.supports_dma()); + assert!(bus.data.features.supports_coherent()); + assert_eq!(bus.data.features.burst_len_log2(), 4); + + // Assert Nested Accelerators via generated getter, and ensure iterator is exhausted + let mut accel_iter = system_tlv.accelerators(); + + // 1. GPU + let accel_gpu = accel_iter.next().unwrap(); + match accel_gpu.config().unwrap() { + AcceleratorConfig::Gpu(gpu_item) => { + assert_eq!(gpu_item.data.cores, 512); + assert_eq!(gpu_item.data.frequency_mhz, 1200); + } + _ => panic!("Expected GPU variant"), + } + assert!(accel_gpu.npu().is_none()); + assert!(accel_gpu.crypto().is_none()); + + // 2. NPU + let accel_npu = accel_iter.next().unwrap(); + match accel_npu.config().unwrap() { + AcceleratorConfig::Npu(npu_item) => { + assert_eq!(npu_item.data.tops, 42); + } + _ => panic!("Expected NPU variant"), + } + assert!(accel_npu.gpu().is_none()); + assert!(accel_npu.crypto().is_none()); + + // 3. Crypto + let accel_crypto = accel_iter.next().unwrap(); + match accel_crypto.config().unwrap() { + AcceleratorConfig::Crypto(crypto_item) => { + assert_eq!(crypto_item.data.key_size_bits, 256); + assert_eq!(crypto_item.supported_ciphers(), b"AES,RSA"); + } + _ => panic!("Expected Crypto variant"), + } + assert!(accel_crypto.gpu().is_none()); + assert!(accel_crypto.npu().is_none()); + + assert!( + accel_iter.next().is_none(), + "Expected exactly three accelerators" + ); + } + + #[test] + fn test_proto_to_tlv_recursive_tree() { + use edge_cases_tlv::NodeExt; + + // Construct a deep recursive binary tree: + // 10 + // / \ + // 5 15 + // / + // 2 + let tree = protobuf::proto!(edge_cases_rust_proto::Node { + value: 10, + children: [ + edge_cases_rust_proto::Node { + value: 5, + children: [edge_cases_rust_proto::Node { value: 2 },], + }, + edge_cases_rust_proto::Node { value: 15 }, + ], + }); + + // Convert to TLV using the edge cases descriptor info + let tlv_words = message_to_tlv(&SHARED_TLV_TEST_EDGE_CASES_DESCRIPTOR_INFO, &tree) + .expect("Failed to convert Node tree to TLV!"); + + // Overlay the container zero-copy + let tlv_data = TlvData::overlay(&tlv_words); + + let mut root_iter = tlv_data.iter::(); + let node_10 = root_iter.next().expect("Failed to get root node"); + assert_eq!(node_10.data.value, 10); + + let mut children_10 = node_10.children(); + + // Verify left child (node 5) + let node_5 = children_10.next().unwrap(); + assert_eq!(node_5.data.value, 5); + + let mut children_5 = node_5.children(); + + // Verify left-left grandchild (node 2) + let node_2 = children_5.next().unwrap(); + assert_eq!(node_2.data.value, 2); + assert!(node_2.children().next().is_none()); + + assert!(children_5.next().is_none()); + + // Verify right child (node 15) + let node_15 = children_10.next().unwrap(); + assert_eq!(node_15.data.value, 15); + assert!(node_15.children().next().is_none()); + + assert!(children_10.next().is_none()); + } + + #[test] + fn test_proto_to_tlv_string_too_long() { + let system = protobuf::proto!(soc_rust_proto::System { + vendor_id: 32898, + product_id: 4660, + total_mem_mib: 65536, + system_name: "ThisSystemNameIsWayTooLongFor16Bytes".to_string(), // 36 bytes! + system_uuid: vec![0xAA, 0xBB, 0xCC, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + }); + + let err = message_to_tlv(&SHARED_TLV_TEST_SOC_DESCRIPTOR_INFO, &system).unwrap_err(); + + assert_eq!( + err.to_string(), + "Value for field 'system_name' is too long (length is 36 bytes, but target packed size in the fixed struct is only 16 bytes)." + ); + } + + #[test] + fn test_proto_to_tlv_string_utf8_fixed() { + use soc_tlv::System; + + let system = protobuf::proto!(soc_rust_proto::System { + vendor_id: 32898, + product_id: 4660, + total_mem_mib: 65536, + system_name: "MySystém".to_string(), // UTF-8 string (9 bytes in UTF-8) + system_uuid: vec![0xAA, 0xBB, 0xCC, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + }); + + let tlv_words = message_to_tlv(&SHARED_TLV_TEST_SOC_DESCRIPTOR_INFO, &system) + .expect("Should cleanly convert inline UTF-8 string without error!"); + + let tlv_data = openprot_tlv::TlvData::overlay(&tlv_words); + let system_tlv = tlv_data.iter::().next().unwrap(); + + let mut expected_name = [0u8; 16]; + expected_name[..9].copy_from_slice("MySystém".as_bytes()); + assert_eq!(system_tlv.data.system_name, expected_name); + } + + #[test] + fn test_proto_to_tlv_string_extension_bstr_utf8() { + use bstr::ByteSlice; + use edge_cases_rust_proto::__unstable::SHARED_TLV_TEST_EDGE_CASES_DESCRIPTOR_INFO; + use edge_cases_tlv::StringExtensionTestExt; + + let msg = protobuf::proto!(edge_cases_rust_proto::StringExtensionTest { + text: "hello bstr 🌍".to_string(), // valid UTF-8 string including emoji + }); + + let tlv_words = message_to_tlv(&SHARED_TLV_TEST_EDGE_CASES_DESCRIPTOR_INFO, &msg) + .expect("Failed to convert StringExtensionTest with UTF-8 to TLV!"); + + let tlv_data = openprot_tlv::TlvData::overlay(&tlv_words); + let item = tlv_data + .iter::() + .next() + .expect("Should decode StringExtensionTest item"); + + // 1. Assert direct comparison with string literal (zero-cost, works directly with UTF-8!) + assert_eq!(item.text(), "hello bstr 🌍"); + + // 2. A willing user can opt-in to validation or conversions using bstr methods + assert_eq!(item.text().to_str().expect("Valid UTF-8"), "hello bstr 🌍"); + } + + #[test] + fn test_proto_to_tlv_bytes_length_mismatch() { + let system = protobuf::proto!(soc_rust_proto::System { + vendor_id: 32898, + product_id: 4660, + total_mem_mib: 65536, + system_name: "MyAwesomeSystem".to_string(), + system_uuid: vec![0xAA, 0xBB, 0xCC], // Only 3 bytes, must be exactly 16! + }); + + let err = message_to_tlv(&SHARED_TLV_TEST_SOC_DESCRIPTOR_INFO, &system).unwrap_err(); + + assert_eq!( + err.to_string(), + "Length of bytes field 'system_uuid' must match the target size exactly (expected 16 bytes, got 3 bytes)." + ); + } + + #[test] + fn test_proto_to_tlv_bit_fields() { + let container = protobuf::proto!(edge_cases_rust_proto::RegisterContainer { + reg: edge_cases_rust_proto::PackedRegister { + enabled: true, // bit 0 = true (1) + mode: 5, // bits 1-3 = 5 (0b101) + status: 9, // bits 4-7 = 9 (0b1001) + val: 123456, // bits 8-31 = 123456 (0x01E240) + }, + reg16: edge_cases_rust_proto::Packed16Register { + flag: true, // bit 0 = true (1) + mode: 6, // bits 1-4 = 6 (0b0110) -> << 1 = 12 + val: 1000, // bits 5-15 = 1000 -> << 5 = 32000 + }, + reg8: edge_cases_rust_proto::Packed8Register { + flag: true, // bit 0 = true (1) + status: 45, // bits 1-7 = 45 (0b0101101) -> << 1 = 90 + }, + }); + + let tlv_words = message_to_tlv(&SHARED_TLV_TEST_EDGE_CASES_DESCRIPTOR_INFO, &container) + .expect("Failed to convert RegisterContainer to TLV!"); + + let mut expected = Vec::new(); + expected.extend_from_slice(b"CONT"); // tag + expected.extend_from_slice(&[0x02, 0x00, 0x00, 0x00]); // word_len: 2, reserved: 0, flags: 0 + + // Combined 32-bit backing word of PackedRegister packed inline at offset 0: + // val (123456 = 0x01E240) << 8 = 0x01E24000 + // status (9 = 0x9) << 4 = 0x00000090 + // mode (5 = 0x5) << 1 = 0x0000000A + // enabled (true = 1) = 0x00000001 + // Total = 0x01E2409B + expected.extend_from_slice(&0x01E2409Bu32.to_le_bytes()); + + // 16-bit backing word of Packed16Register packed inline at offset 4: + // val (1000) << 5 = 32000 + // mode (6) << 1 = 12 + // flag (1) = 1 + // Total u16 = 32013 (0x7D0D) + expected.extend_from_slice(&32013u16.to_le_bytes()); + + // 8-bit backing byte of Packed8Register packed inline at offset 6: + // status (45) << 1 = 90 + // flag (1) = 1 + // Total u8 = 91 (0x5B) + expected.push(91u8); + + // Padding byte to align 8-byte container struct length (offset 7) + expected.push(0u8); + + assert_eq!(tlv_words.as_bytes(), expected); + } + + #[test] + fn test_proto_to_tlv_sfixed_negative() { + let msg = protobuf::proto!(edge_cases_rust_proto::SfixedTest { + sfix: -5, + sfix_i16: -100, + sfix_i8: -12, + }); + + let tlv_words = message_to_tlv(&SHARED_TLV_TEST_EDGE_CASES_DESCRIPTOR_INFO, &msg) + .expect("Failed to convert SfixedTest to TLV!"); + + let mut expected = Vec::new(); + expected.extend_from_slice(b"SFIX"); // tag + expected.extend_from_slice(&[0x02, 0x00, 0x00, 0x00]); // word_len: 2, reserved: 0, flags: 0 + expected.extend_from_slice(&(-5i32).to_le_bytes()); // sfix (4 bytes) + expected.extend_from_slice(&(-100i16).to_le_bytes()); // sfix_i16 (2 bytes) + expected.push(-12i8 as u8); // sfix_i8 (1 byte) + expected.push(0); // body padding to 8 bytes + + assert_eq!(tlv_words.as_bytes(), expected); + } + + #[test] + fn test_proto_to_tlv_extra_data() { + use edge_cases_rust_proto::__unstable::SHARED_TLV_TEST_EDGE_CASES_DESCRIPTOR_INFO; + use edge_cases_tlv::RawBytesExtensionTestExt; + + let version_msg = protobuf::proto!(edge_cases_rust_proto::RawBytesExtensionTest { + payload: b"1.2.3".to_vec(), + }); + + let tlv_words = message_to_tlv(&SHARED_TLV_TEST_EDGE_CASES_DESCRIPTOR_INFO, &version_msg) + .expect("Failed to convert RawBytesExtensionTest to TLV!"); + + // Expected header: + // Tag: 'RBXT' + // word_len: 2 words (8 bytes) + // flags: padding_bytes = 3 (8 - 5 = 3) + let mut expected = Vec::new(); + expected.extend_from_slice(b"RBXT"); + expected.extend_from_slice(&[0x02, 0x00, 0x00, 0x03]); + expected.extend_from_slice(b"1.2.3\0\0\0"); + + assert_eq!(tlv_words.as_bytes(), expected); + + // Verify overlay & accessor round-trip decoding + let tlv_data = openprot_tlv::TlvData::overlay(&tlv_words); + let item = tlv_data + .iter::() + .next() + .expect("Should decode RawBytesExtensionTest item"); + + assert_eq!(item.payload(), b"1.2.3"); + } + + #[test] + fn test_proto_to_tlv_extra_data_nonzero_offset() { + use soc_rust_proto::__unstable::SHARED_TLV_TEST_SOC_DESCRIPTOR_INFO; + use soc_tlv::CryptoAcceleratorExt; + + let msg = protobuf::proto!(soc_rust_proto::CryptoAccelerator { + key_size_bits: 512, + supported_ciphers: b"CHACHA20-POLY1305".to_vec(), + }); + + let tlv_words = message_to_tlv(&SHARED_TLV_TEST_SOC_DESCRIPTOR_INFO, &msg) + .expect("Failed to convert CryptoAccelerator to TLV!"); + + // Expected header: + // Tag: 'CRYP' + // Fixed field key_size_bits: 4 bytes (offset 0..4). + // supported_ciphers: 17 bytes (offset 4..21). + // Total unpadded payload: 21 bytes. Padded: 24 bytes (6 words). Padding bytes: 3. + let mut expected = Vec::new(); + expected.extend_from_slice(b"CRYP"); + expected.extend_from_slice(&[0x06, 0x00, 0x00, 0x03]); // word_len: 6, reserved: 0, flags: 3 + expected.extend_from_slice(&512u32.to_le_bytes()); + expected.extend_from_slice(b"CHACHA20-POLY1305\0\0\0"); + + assert_eq!(tlv_words.as_bytes(), expected); + + let tlv_data = openprot_tlv::TlvData::overlay(&tlv_words); + let item = tlv_data + .iter::() + .next() + .expect("Should decode CryptoAccelerator item"); + + assert_eq!(item.data.key_size_bits, 512); + assert_eq!(item.supported_ciphers(), b"CHACHA20-POLY1305"); + } + + #[test] + fn test_proto_to_tlv_nested_unaligned_extra_data() { + use edge_cases_rust_proto::__unstable::SHARED_TLV_TEST_EDGE_CASES_DESCRIPTOR_INFO; + use edge_cases_tlv::*; + + let container_msg = protobuf::proto!(edge_cases_rust_proto::ExtensionContainer { + id: 42, + child_ext: protobuf::proto!(edge_cases_rust_proto::RawBytesExtensionTest { + payload: b"1.2.3-unaligned_suffix".to_vec(), // 22 bytes (22 % 4 == 2) + }), + next_child: protobuf::proto!(edge_cases_rust_proto::RegisterContainer { + reg: protobuf::proto!(edge_cases_rust_proto::PackedRegister { + enabled: true, + mode: 5, + status: 9, + val: 123456, + }), + reg16: protobuf::proto!(edge_cases_rust_proto::Packed16Register { + flag: true, + mode: 6, + val: 1000, + }), + reg8: protobuf::proto!(edge_cases_rust_proto::Packed8Register { + flag: true, + status: 45, + }), + }), + }); + + let tlv_words = + message_to_tlv(&SHARED_TLV_TEST_EDGE_CASES_DESCRIPTOR_INFO, &container_msg) + .expect("Failed to convert ExtensionContainer to TLV!"); + + // Overlay TlvData and parse top-level container + let tlv_data = openprot_tlv::TlvData::overlay(&tlv_words); + let container_item = tlv_data + .iter::() + .next() + .expect("Should decode ExtensionContainer"); + + assert_eq!(container_item.data.id, 42); + + // Parse child RawBytesExtensionTest + let child_ext_item = container_item + .child_ext() + .expect("Should decode RawBytesExtensionTest"); + + // Assert unpadded raw bytes match exact 22 bytes + assert_eq!(child_ext_item.payload(), b"1.2.3-unaligned_suffix"); + + // Assert RegisterContainer sub-TLV following RawBytesExtensionTest is correctly word-aligned and decoded + let next_child_item = container_item + .next_child() + .expect("Should decode RegisterContainer sub-TLV after RawBytesExtensionTest"); + + // Explicitly assert pointer alignment of next_child_item's struct + let next_child_ptr = (&next_child_item.data as *const _) as usize; + assert_eq!( + next_child_ptr % 4, + 0, + "Next child pointer must be 4-byte aligned!" + ); + + assert_eq!(next_child_item.data.reg.val(), 123456); + assert_eq!(next_child_item.data.reg16.val(), 1000); + assert_eq!(next_child_item.data.reg8.status(), 45); + } + + #[test] + fn test_proto_to_tlv_extra_data_duplicate_field() { + use edge_cases_rust_proto::__unstable::SHARED_TLV_TEST_EDGE_CASES_DESCRIPTOR_INFO; + use edge_cases_tlv::RawBytesExtensionTestExt; + use openprot_tlv_proto_meta::collect_descriptor_set; + use openprot_tlv_proto_meta::find_message_descriptor; + + let desc_set = + collect_descriptor_set(&SHARED_TLV_TEST_EDGE_CASES_DESCRIPTOR_INFO).unwrap(); + let msg_desc = find_message_descriptor( + &desc_set, + "openprot.tlv.test.edge_cases.RawBytesExtensionTest", + ) + .unwrap(); + + // Wire tag for field #1 with length-delimited wire type 2: (1 << 3) | 2 = 0x0A + let mut duplicate_wire_bytes = Vec::new(); + // First occurrence: payload = "v1" + duplicate_wire_bytes.extend_from_slice(&[0x0A, 0x02, b'v', b'1']); + // Second occurrence (duplicate): payload = "v2" + duplicate_wire_bytes.extend_from_slice(&[0x0A, 0x02, b'v', b'2']); + + let mut tlv_words = Vec::new(); + encode_message(&desc_set, msg_desc, &duplicate_wire_bytes, &mut tlv_words) + .expect("Failed to encode duplicate wire bytes!"); + + // Overlay TlvData and parse + let tlv_data = openprot_tlv::TlvData::overlay(&tlv_words); + let item = tlv_data + .iter::() + .next() + .expect("Should decode RawBytesExtensionTest item"); + + // Protobuf spec rule: last occurrence wins. + // Expected payload is b"v2" of length 2. + assert_eq!(item.payload(), b"v2"); + } + + #[test] + fn test_proto_to_tlv_unknown_fields() { + let msg = protobuf::proto!(edge_cases_rust_proto::SfixedTest { + sfix: -5, + sfix_i16: -100, + sfix_i8: -12, + }); + + let encoded_data = msg.serialize().expect("Failed to serialize SfixedTest"); + + let mut proto_bytes = Vec::new(); + + // Inject unknown varint field at the BEGINNING (field #777, varint wire type 0 => tag 6216) + // 6216 in varint encoding: 6216 = (777 << 3) | 0 = 0x1848 -> [0xC8, 0x30] + proto_bytes.extend_from_slice(&[0xC8, 0x30, 0x42]); // tag 6216, value 42 + + // Inject unknown length-delimited field at the BEGINNING (field #666, wire type 2 => tag 5330) + // 5330 in varint encoding: 5330 = (666 << 3) | 2 = 0x14D2 -> [0xD2, 0x2A] + proto_bytes.extend_from_slice(&[0xD2, 0x2A, 0x06]); // tag 5330, len 6 + proto_bytes.extend_from_slice(b"header"); + + // Insert the actual encoded data in the middle + proto_bytes.extend_from_slice(&encoded_data); + + // Inject unknown varint field at the END (field #999, varint wire type 0 => tag 7992) + // 7992 in varint encoding: 7992 = (999 << 3) | 0 = 0x1F38 -> [0xB8, 0x3E] + proto_bytes.extend_from_slice(&[0xB8, 0x3E, 0x99, 0x01]); // tag 7992, value 153 + + // Inject unknown length-delimited field at the END (field #888, wire type 2 => tag 7106) + // 7106 in varint encoding: 7106 = (888 << 3) | 2 = 0x1BC2 -> [0xC2, 0x37] + proto_bytes.extend_from_slice(&[0xC2, 0x37, 0x07]); // tag 7106, len 7 + proto_bytes.extend_from_slice(b"unknown"); + + let tlv_words = proto_to_tlv_with_info( + &SHARED_TLV_TEST_EDGE_CASES_DESCRIPTOR_INFO, + std::any::type_name::(), + &proto_bytes, + ) + .expect("Failed to convert proto with unknown fields to TLV!"); + + let mut expected = Vec::new(); + expected.extend_from_slice(b"SFIX"); // tag + expected.extend_from_slice(&[0x02, 0x00, 0x00, 0x00]); // word_len: 2, reserved: 0, flags: 0 + expected.extend_from_slice(&(-5i32).to_le_bytes()); // sfix (4 bytes) + expected.extend_from_slice(&(-100i16).to_le_bytes()); // sfix_i16 (2 bytes) + expected.push(-12i8 as u8); // sfix_i8 (1 byte) + expected.push(0); // body padding to 8 bytes + + assert_eq!(tlv_words.as_bytes(), expected); + } + + #[test] + fn test_nested_message_collision() { + let msg = protobuf::proto!(edge_cases_rust_proto::MessageCollisionContainer { + local: edge_cases_rust_proto::message_collision_container::PackedRegister { val: 42 } + }); + + let tlv_words = message_to_tlv(&SHARED_TLV_TEST_EDGE_CASES_DESCRIPTOR_INFO, &msg) + .expect("Failed to convert MessageCollisionContainer to TLV!"); + + let mut expected = Vec::new(); + expected.extend_from_slice(b"COLL"); // tag + expected.extend_from_slice(&[0x01, 0x00, 0x00, 0x00]); // word_len: 1, reserved: 0, flags: 0 + expected.extend_from_slice(&42u32.to_le_bytes()); // val at offset 0 + + assert_eq!(tlv_words.as_bytes(), expected); + } + + #[test] + fn test_direct_nested_message_serialization() { + let msg = protobuf::proto!( + edge_cases_rust_proto::message_collision_container::PackedRegister { val: 99 } + ); + + let tlv_words = message_to_tlv(&SHARED_TLV_TEST_EDGE_CASES_DESCRIPTOR_INFO, &msg) + .expect("Failed to convert nested PackedRegister directly to TLV!"); + + let mut expected = Vec::new(); + expected.extend_from_slice(b"NPK_"); // tag + expected.extend_from_slice(&[0x01, 0x00, 0x00, 0x00]); // word_len: 1, reserved: 0, flags: 0 + expected.extend_from_slice(&99u32.to_le_bytes()); // val at offset 0 + + assert_eq!(tlv_words.as_bytes(), expected); + } + + #[test] + fn test_proto_to_tlv_floats() { + let msg = protobuf::proto!(edge_cases_rust_proto::FloatTest { + fval: -1.5, + dval: -2.75, + }); + + let tlv_words = message_to_tlv(&SHARED_TLV_TEST_EDGE_CASES_DESCRIPTOR_INFO, &msg) + .expect("Failed to convert FloatTest to TLV!"); + + let mut expected = Vec::new(); + expected.extend_from_slice(b"FLOT"); // tag + expected.extend_from_slice(&[0x04, 0x00, 0x00, 0x00]); // word_len: 4, reserved: 0, flags: 0 + expected.extend_from_slice(&(-1.5f32).to_bits().to_le_bytes()); // fval (4 bytes) + expected.extend_from_slice(&[0u8; 4]); // padding + expected.extend_from_slice(&(-2.75f64).to_bits().to_le_bytes()); // dval (8 bytes) + + assert_eq!(tlv_words.as_bytes(), expected); + } + + #[test] + fn test_proto_to_tlv_u64_alignment() { + let msg = protobuf::proto!(edge_cases_rust_proto::U64AlignmentTest { + val32: 0x12345678, + val64: 0x0102030405060708, + }); + + let tlv_words = message_to_tlv(&SHARED_TLV_TEST_EDGE_CASES_DESCRIPTOR_INFO, &msg) + .expect("Failed to convert U64AlignmentTest to TLV!"); + + let mut expected = Vec::new(); + expected.extend_from_slice(b"ALGN"); // tag + expected.extend_from_slice(&[0x03, 0x00, 0x00, 0x00]); // word_len: 3, reserved: 0, flags: 0 + expected.extend_from_slice(&0x12345678u32.to_le_bytes()); // val32 (4 bytes, offset 0) + expected.extend_from_slice(&0x0102030405060708u64.to_le_bytes()); // val64 (8 bytes, offset 4) + + assert_eq!(tlv_words.as_bytes(), expected); + } + + #[test] + fn test_proto_to_tlv_sibling_inline_reuse() { + let msg = protobuf::proto!(edge_cases_rust_proto::SiblingInlineParent { + child1: edge_cases_rust_proto::SiblingInlineChild { val: 100 }, + child2: edge_cases_rust_proto::SiblingInlineChild { val: 200 }, + }); + + let tlv_words = message_to_tlv(&SHARED_TLV_TEST_EDGE_CASES_DESCRIPTOR_INFO, &msg) + .expect("Failed to convert SiblingInlineParent to TLV!"); + + // Verify we can parse it back successfully + let tlv_data = openprot_tlv::TlvData::overlay(&tlv_words); + let mut iter = tlv_data.iter::(); + let parent_item = iter.next().expect("Should have one SiblingInlineParent"); + assert!(iter.next().is_none()); + + assert_eq!( + parent_item.container.header.tag, + u32::from_le_bytes(*b"SIBR") + ); + assert_eq!(parent_item.data.child1.val, 100); + assert_eq!(parent_item.data.child2.val, 200); + } + + #[track_caller] + fn run_all_types_test( + msg: edge_cases_rust_proto::AllTypesTest, + expected_struct: edge_cases_tlv::AllTypesTest, + ) { + let tlv_words = message_to_tlv(&SHARED_TLV_TEST_EDGE_CASES_DESCRIPTOR_INFO, &msg) + .expect("Failed to convert AllTypesTest to TLV!"); + + let tlv_data = openprot_tlv::TlvData::overlay(&tlv_words); + let decoded = tlv_data + .iter::() + .next() + .unwrap(); + + assert_eq!(*decoded.data, expected_struct); + } + + #[track_caller] + fn assert_overflow(modify: F, expected_err: &str) + where + F: FnOnce(&mut edge_cases_rust_proto::AllTypesTest), + { + let mut msg = edge_cases_rust_proto::AllTypesTest::new(); + modify(&mut msg); + let err = message_to_tlv(&SHARED_TLV_TEST_EDGE_CASES_DESCRIPTOR_INFO, &msg).unwrap_err(); + assert_eq!(err.to_string(), expected_err); + } + + #[test] + fn test_proto_to_tlv_all_types() { + // Test case 1: Happy path with various typical custom-sized integer and float/double types + run_all_types_test( + protobuf::proto!(edge_cases_rust_proto::AllTypesTest { + sint32_i8: -1, + uint32_u8: 2, + int32_i8: -3, + int64_i8: -4, + sfixed32_i8: -5, + fixed32_u8: 6, + sint64_i8: -7, + uint64_u8: 8, + bool_val: true, + sint32_i16: -100, + uint32_u16: 200, + int32_i16: -300, + int64_i16: -400, + sfixed32_i16: -500, + fixed32_u16: 600, + sint64_i16: -700, + uint64_u16: 800, + sint32_i32: -10000, + uint32_u32: 20000, + int32_i32: -30000, + sfixed32_i32: -40000, + fixed32_u32: 50000, + packed_reg: edge_cases_rust_proto::PackedSignedRegister { + sval: -5, + uval: 200, + flag: true, + sfix: -100, + }, + float_val: 12.5, + sint64_i32: -60000, + uint64_u32: 70000, + int64_i32: -80000, + sint64_i64: -900000000, + uint64_u64: 1000000000, + int64_i64: -1100000000, + double_val: -123.456, + }), + edge_cases_tlv::AllTypesTest { + sint32_i8: -1, + uint32_u8: 2, + int32_i8: -3, + int64_i8: -4, + sfixed32_i8: -5, + fixed32_u8: 6, + sint64_i8: -7, + uint64_u8: 8, + bool_val: 1, + sint32_i16: -100, + uint32_u16: 200, + int32_i16: -300, + int64_i16: -400, + sfixed32_i16: -500, + fixed32_u16: 600, + sint64_i16: -700, + uint64_u16: 800, + sint32_i32: -10000, + uint32_u32: 20000, + int32_i32: -30000, + sfixed32_i32: -40000, + fixed32_u32: 50000, + packed_reg: edge_cases_tlv::PackedSignedRegister::new() + .with_sval(-5) + .with_uval(200) + .with_flag(true) + .with_sfix(-100), + float_val: 12.5, + sint64_i32: -60000, + uint64_u32: 70000, + int64_i32: -80000, + sint64_i64: (-900000000i64).into(), + uint64_u64: 1000000000u64.into(), + int64_i64: (-1100000000i64).into(), + double_val: (-123.456f64).into(), + ..Default::default() + }, + ); + + // Test case 2: Minimum boundary values for all fields + run_all_types_test( + protobuf::proto!(edge_cases_rust_proto::AllTypesTest { + sint32_i8: -128, + uint32_u8: 0, + int32_i8: -128, + int64_i8: -128, + sfixed32_i8: -128, + fixed32_u8: 0, + sint64_i8: -128, + uint64_u8: 0, + bool_val: false, + sint32_i16: -32768, + uint32_u16: 0, + int32_i16: -32768, + int64_i16: -32768, + sfixed32_i16: -32768, + fixed32_u16: 0, + sint64_i16: -32768, + uint64_u16: 0, + sint32_i32: -2147483648, + uint32_u32: 0, + int32_i32: -2147483648, + sfixed32_i32: -2147483648, + fixed32_u32: 0, + packed_reg: edge_cases_rust_proto::PackedSignedRegister { + sval: -128, + uval: 0, + flag: false, + sfix: -1024, + }, + float_val: f32::MIN, + sint64_i32: -2147483648, + uint64_u32: 0, + int64_i32: -2147483648, + sint64_i64: i64::MIN, + uint64_u64: 0, + int64_i64: i64::MIN, + double_val: f64::MIN, + }), + edge_cases_tlv::AllTypesTest { + sint32_i8: -128, + uint32_u8: 0, + int32_i8: -128, + int64_i8: -128, + sfixed32_i8: -128, + fixed32_u8: 0, + sint64_i8: -128, + uint64_u8: 0, + bool_val: 0, + sint32_i16: -32768, + uint32_u16: 0, + int32_i16: -32768, + int64_i16: -32768, + sfixed32_i16: -32768, + fixed32_u16: 0, + sint64_i16: -32768, + uint64_u16: 0, + sint32_i32: -2147483648, + uint32_u32: 0, + int32_i32: -2147483648, + sfixed32_i32: -2147483648, + fixed32_u32: 0, + packed_reg: edge_cases_tlv::PackedSignedRegister::new() + .with_sval(-128) + .with_uval(0) + .with_flag(false) + .with_sfix(-1024), + float_val: f32::MIN, + sint64_i32: -2147483648, + uint64_u32: 0, + int64_i32: -2147483648, + sint64_i64: i64::MIN.into(), + uint64_u64: 0u64.into(), + int64_i64: i64::MIN.into(), + double_val: f64::MIN.into(), + ..Default::default() + }, + ); + + // Test case 3: Maximum boundary values for all fields + run_all_types_test( + protobuf::proto!(edge_cases_rust_proto::AllTypesTest { + sint32_i8: 127, + uint32_u8: 255, + int32_i8: 127, + int64_i8: 127, + sfixed32_i8: 127, + fixed32_u8: 255, + sint64_i8: 127, + uint64_u8: 255, + bool_val: true, + sint32_i16: 32767, + uint32_u16: 65535, + int32_i16: 32767, + int64_i16: 32767, + sfixed32_i16: 32767, + fixed32_u16: 65535, + sint64_i16: 32767, + uint64_u16: 65535, + sint32_i32: 2147483647, + uint32_u32: 4294967295, + int32_i32: 2147483647, + sfixed32_i32: 2147483647, + fixed32_u32: 4294967295, + packed_reg: edge_cases_rust_proto::PackedSignedRegister { + sval: 127, + uval: 255, + flag: true, + sfix: 1023, + }, + float_val: f32::MAX, + sint64_i32: 2147483647, + uint64_u32: 4294967295, + int64_i32: 2147483647, + sint64_i64: i64::MAX, + uint64_u64: u64::MAX, + int64_i64: i64::MAX, + double_val: f64::MAX, + }), + edge_cases_tlv::AllTypesTest { + sint32_i8: 127, + uint32_u8: 255, + int32_i8: 127, + int64_i8: 127, + sfixed32_i8: 127, + fixed32_u8: 255, + sint64_i8: 127, + uint64_u8: 255, + bool_val: 1, + sint32_i16: 32767, + uint32_u16: 65535, + int32_i16: 32767, + int64_i16: 32767, + sfixed32_i16: 32767, + fixed32_u16: 65535, + sint64_i16: 32767, + uint64_u16: 65535, + sint32_i32: 2147483647, + uint32_u32: 4294967295, + int32_i32: 2147483647, + sfixed32_i32: 2147483647, + fixed32_u32: 4294967295, + packed_reg: edge_cases_tlv::PackedSignedRegister::new() + .with_sval(127) + .with_uval(255) + .with_flag(true) + .with_sfix(1023), + float_val: f32::MAX, + sint64_i32: 2147483647, + uint64_u32: 4294967295, + int64_i32: 2147483647, + sint64_i64: i64::MAX.into(), + uint64_u64: u64::MAX.into(), + int64_i64: i64::MAX.into(), + double_val: f64::MAX.into(), + ..Default::default() + }, + ); + + // --- 8-bit Signed Fields (overflow, underflow, and protobuf limits) --- + + // sint32_i8 (protobuf i32, packed i8) + assert_overflow( + |m| m.set_sint32_i8(128), + "Value 128 for field 'sint32_i8' is out of range for its packed bit-width of 8 bits (valid range is -128..=127).", + ); + assert_overflow( + |m| m.set_sint32_i8(-129), + "Value -129 for field 'sint32_i8' is out of range for its packed bit-width of 8 bits (valid range is -128..=127).", + ); + assert_overflow( + |m| m.set_sint32_i8(i32::MAX), + "Value 2147483647 for field 'sint32_i8' is out of range for its packed bit-width of 8 bits (valid range is -128..=127).", + ); + assert_overflow( + |m| m.set_sint32_i8(i32::MIN), + "Value -2147483648 for field 'sint32_i8' is out of range for its packed bit-width of 8 bits (valid range is -128..=127).", + ); + + // int32_i8 (protobuf i32, packed i8) + assert_overflow( + |m| m.set_int32_i8(128), + "Value 128 for field 'int32_i8' is out of range for its packed bit-width of 8 bits (valid range is -128..=127).", + ); + assert_overflow( + |m| m.set_int32_i8(-129), + "Value -129 for field 'int32_i8' is out of range for its packed bit-width of 8 bits (valid range is -128..=127).", + ); + assert_overflow( + |m| m.set_int32_i8(i32::MAX), + "Value 2147483647 for field 'int32_i8' is out of range for its packed bit-width of 8 bits (valid range is -128..=127).", + ); + assert_overflow( + |m| m.set_int32_i8(i32::MIN), + "Value -2147483648 for field 'int32_i8' is out of range for its packed bit-width of 8 bits (valid range is -128..=127).", + ); + + // int64_i8 (protobuf i64, packed i8) + assert_overflow( + |m| m.set_int64_i8(128), + "Value 128 for field 'int64_i8' is out of range for its packed bit-width of 8 bits (valid range is -128..=127).", + ); + assert_overflow( + |m| m.set_int64_i8(-129), + "Value -129 for field 'int64_i8' is out of range for its packed bit-width of 8 bits (valid range is -128..=127).", + ); + assert_overflow( + |m| m.set_int64_i8(i64::MAX), + "Value 9223372036854775807 for field 'int64_i8' is out of range for its packed bit-width of 8 bits (valid range is -128..=127).", + ); + assert_overflow( + |m| m.set_int64_i8(i64::MIN), + "Value -9223372036854775808 for field 'int64_i8' is out of range for its packed bit-width of 8 bits (valid range is -128..=127).", + ); + + // sfixed32_i8 (protobuf i32, packed i8) + assert_overflow( + |m| m.set_sfixed32_i8(128), + "Value 128 for field 'sfixed32_i8' is out of range for its packed bit-width of 8 bits (valid range is -128..=127).", + ); + assert_overflow( + |m| m.set_sfixed32_i8(-129), + "Value -129 for field 'sfixed32_i8' is out of range for its packed bit-width of 8 bits (valid range is -128..=127).", + ); + assert_overflow( + |m| m.set_sfixed32_i8(i32::MAX), + "Value 2147483647 for field 'sfixed32_i8' is out of range for its packed bit-width of 8 bits (valid range is -128..=127).", + ); + assert_overflow( + |m| m.set_sfixed32_i8(i32::MIN), + "Value -2147483648 for field 'sfixed32_i8' is out of range for its packed bit-width of 8 bits (valid range is -128..=127).", + ); + + // sint64_i8 (protobuf i64, packed i8) + assert_overflow( + |m| m.set_sint64_i8(128), + "Value 128 for field 'sint64_i8' is out of range for its packed bit-width of 8 bits (valid range is -128..=127).", + ); + assert_overflow( + |m| m.set_sint64_i8(-129), + "Value -129 for field 'sint64_i8' is out of range for its packed bit-width of 8 bits (valid range is -128..=127).", + ); + assert_overflow( + |m| m.set_sint64_i8(i64::MAX), + "Value 9223372036854775807 for field 'sint64_i8' is out of range for its packed bit-width of 8 bits (valid range is -128..=127).", + ); + assert_overflow( + |m| m.set_sint64_i8(i64::MIN), + "Value -9223372036854775808 for field 'sint64_i8' is out of range for its packed bit-width of 8 bits (valid range is -128..=127).", + ); + + // --- 8-bit Unsigned Fields (overflow and protobuf limits) --- + + // uint32_u8 (protobuf u32, packed u8) + assert_overflow( + |m| m.set_uint32_u8(256), + "Value 256 for field 'uint32_u8' is out of range for its packed bit-width of 8 bits (valid range is 0..=255).", + ); + assert_overflow( + |m| m.set_uint32_u8(u32::MAX), + "Value 4294967295 for field 'uint32_u8' is out of range for its packed bit-width of 8 bits (valid range is 0..=255).", + ); + + // fixed32_u8 (protobuf u32, packed u8) + assert_overflow( + |m| m.set_fixed32_u8(256), + "Value 256 for field 'fixed32_u8' is out of range for its packed bit-width of 8 bits (valid range is 0..=255).", + ); + assert_overflow( + |m| m.set_fixed32_u8(u32::MAX), + "Value 4294967295 for field 'fixed32_u8' is out of range for its packed bit-width of 8 bits (valid range is 0..=255).", + ); + + // uint64_u8 (protobuf u64, packed u8) + assert_overflow( + |m| m.set_uint64_u8(256), + "Value 256 for field 'uint64_u8' is out of range for its packed bit-width of 8 bits (valid range is 0..=255).", + ); + assert_overflow( + |m| m.set_uint64_u8(u64::MAX), + "Value 18446744073709551615 for field 'uint64_u8' is out of range for its packed bit-width of 8 bits (valid range is 0..=255).", + ); + + // --- 16-bit Signed Fields (overflow, underflow, and protobuf limits) --- + + // sint32_i16 (protobuf i32, packed i16) + assert_overflow( + |m| m.set_sint32_i16(32768), + "Value 32768 for field 'sint32_i16' is out of range for its packed bit-width of 16 bits (valid range is -32768..=32767).", + ); + assert_overflow( + |m| m.set_sint32_i16(-32769), + "Value -32769 for field 'sint32_i16' is out of range for its packed bit-width of 16 bits (valid range is -32768..=32767).", + ); + assert_overflow( + |m| m.set_sint32_i16(i32::MAX), + "Value 2147483647 for field 'sint32_i16' is out of range for its packed bit-width of 16 bits (valid range is -32768..=32767).", + ); + assert_overflow( + |m| m.set_sint32_i16(i32::MIN), + "Value -2147483648 for field 'sint32_i16' is out of range for its packed bit-width of 16 bits (valid range is -32768..=32767).", + ); + + // int32_i16 (protobuf i32, packed i16) + assert_overflow( + |m| m.set_int32_i16(32768), + "Value 32768 for field 'int32_i16' is out of range for its packed bit-width of 16 bits (valid range is -32768..=32767).", + ); + assert_overflow( + |m| m.set_int32_i16(-32769), + "Value -32769 for field 'int32_i16' is out of range for its packed bit-width of 16 bits (valid range is -32768..=32767).", + ); + assert_overflow( + |m| m.set_int32_i16(i32::MAX), + "Value 2147483647 for field 'int32_i16' is out of range for its packed bit-width of 16 bits (valid range is -32768..=32767).", + ); + assert_overflow( + |m| m.set_int32_i16(i32::MIN), + "Value -2147483648 for field 'int32_i16' is out of range for its packed bit-width of 16 bits (valid range is -32768..=32767).", + ); + + // int64_i16 (protobuf i64, packed i16) + assert_overflow( + |m| m.set_int64_i16(32768), + "Value 32768 for field 'int64_i16' is out of range for its packed bit-width of 16 bits (valid range is -32768..=32767).", + ); + assert_overflow( + |m| m.set_int64_i16(-32769), + "Value -32769 for field 'int64_i16' is out of range for its packed bit-width of 16 bits (valid range is -32768..=32767).", + ); + assert_overflow( + |m| m.set_int64_i16(i64::MAX), + "Value 9223372036854775807 for field 'int64_i16' is out of range for its packed bit-width of 16 bits (valid range is -32768..=32767).", + ); + assert_overflow( + |m| m.set_int64_i16(i64::MIN), + "Value -9223372036854775808 for field 'int64_i16' is out of range for its packed bit-width of 16 bits (valid range is -32768..=32767).", + ); + + // sfixed32_i16 (protobuf i32, packed i16) + assert_overflow( + |m| m.set_sfixed32_i16(32768), + "Value 32768 for field 'sfixed32_i16' is out of range for its packed bit-width of 16 bits (valid range is -32768..=32767).", + ); + assert_overflow( + |m| m.set_sfixed32_i16(-32769), + "Value -32769 for field 'sfixed32_i16' is out of range for its packed bit-width of 16 bits (valid range is -32768..=32767).", + ); + assert_overflow( + |m| m.set_sfixed32_i16(i32::MAX), + "Value 2147483647 for field 'sfixed32_i16' is out of range for its packed bit-width of 16 bits (valid range is -32768..=32767).", + ); + assert_overflow( + |m| m.set_sfixed32_i16(i32::MIN), + "Value -2147483648 for field 'sfixed32_i16' is out of range for its packed bit-width of 16 bits (valid range is -32768..=32767).", + ); + + // sint64_i16 (protobuf i64, packed i16) + assert_overflow( + |m| m.set_sint64_i16(32768), + "Value 32768 for field 'sint64_i16' is out of range for its packed bit-width of 16 bits (valid range is -32768..=32767).", + ); + assert_overflow( + |m| m.set_sint64_i16(-32769), + "Value -32769 for field 'sint64_i16' is out of range for its packed bit-width of 16 bits (valid range is -32768..=32767).", + ); + assert_overflow( + |m| m.set_sint64_i16(i64::MAX), + "Value 9223372036854775807 for field 'sint64_i16' is out of range for its packed bit-width of 16 bits (valid range is -32768..=32767).", + ); + assert_overflow( + |m| m.set_sint64_i16(i64::MIN), + "Value -9223372036854775808 for field 'sint64_i16' is out of range for its packed bit-width of 16 bits (valid range is -32768..=32767).", + ); + + // --- 16-bit Unsigned Fields (overflow and protobuf limits) --- + + // uint32_u16 (protobuf u32, packed u16) + assert_overflow( + |m| m.set_uint32_u16(65536), + "Value 65536 for field 'uint32_u16' is out of range for its packed bit-width of 16 bits (valid range is 0..=65535).", + ); + assert_overflow( + |m| m.set_uint32_u16(u32::MAX), + "Value 4294967295 for field 'uint32_u16' is out of range for its packed bit-width of 16 bits (valid range is 0..=65535).", + ); + + // fixed32_u16 (protobuf u32, packed u16) + assert_overflow( + |m| m.set_fixed32_u16(65536), + "Value 65536 for field 'fixed32_u16' is out of range for its packed bit-width of 16 bits (valid range is 0..=65535).", + ); + assert_overflow( + |m| m.set_fixed32_u16(u32::MAX), + "Value 4294967295 for field 'fixed32_u16' is out of range for its packed bit-width of 16 bits (valid range is 0..=65535).", + ); + + // uint64_u16 (protobuf u64, packed u16) + assert_overflow( + |m| m.set_uint64_u16(65536), + "Value 65536 for field 'uint64_u16' is out of range for its packed bit-width of 16 bits (valid range is 0..=65535).", + ); + assert_overflow( + |m| m.set_uint64_u16(u64::MAX), + "Value 18446744073709551615 for field 'uint64_u16' is out of range for its packed bit-width of 16 bits (valid range is 0..=65535).", + ); + + // --- 32-bit Signed Fields (overflow, underflow, and protobuf limits) --- + + // sint64_i32 (protobuf i64, packed i32) + assert_overflow( + |m| m.set_sint64_i32(2147483648), + "Value 2147483648 for field 'sint64_i32' is out of range for its packed bit-width of 32 bits (valid range is -2147483648..=2147483647).", + ); + assert_overflow( + |m| m.set_sint64_i32(-2147483649), + "Value -2147483649 for field 'sint64_i32' is out of range for its packed bit-width of 32 bits (valid range is -2147483648..=2147483647).", + ); + assert_overflow( + |m| m.set_sint64_i32(i64::MAX), + "Value 9223372036854775807 for field 'sint64_i32' is out of range for its packed bit-width of 32 bits (valid range is -2147483648..=2147483647).", + ); + assert_overflow( + |m| m.set_sint64_i32(i64::MIN), + "Value -9223372036854775808 for field 'sint64_i32' is out of range for its packed bit-width of 32 bits (valid range is -2147483648..=2147483647).", + ); + + // int64_i32 (protobuf i64, packed i32) + assert_overflow( + |m| m.set_int64_i32(2147483648), + "Value 2147483648 for field 'int64_i32' is out of range for its packed bit-width of 32 bits (valid range is -2147483648..=2147483647).", + ); + assert_overflow( + |m| m.set_int64_i32(-2147483649), + "Value -2147483649 for field 'int64_i32' is out of range for its packed bit-width of 32 bits (valid range is -2147483648..=2147483647).", + ); + assert_overflow( + |m| m.set_int64_i32(i64::MAX), + "Value 9223372036854775807 for field 'int64_i32' is out of range for its packed bit-width of 32 bits (valid range is -2147483648..=2147483647).", + ); + assert_overflow( + |m| m.set_int64_i32(i64::MIN), + "Value -9223372036854775808 for field 'int64_i32' is out of range for its packed bit-width of 32 bits (valid range is -2147483648..=2147483647).", + ); + + // --- 32-bit Unsigned Fields (overflow and protobuf limits) --- + + // uint64_u32 (protobuf u64, packed u32) + assert_overflow( + |m| m.set_uint64_u32(4294967296), + "Value 4294967296 for field 'uint64_u32' is out of range for its packed bit-width of 32 bits (valid range is 0..=4294967295).", + ); + assert_overflow( + |m| m.set_uint64_u32(u64::MAX), + "Value 18446744073709551615 for field 'uint64_u32' is out of range for its packed bit-width of 32 bits (valid range is 0..=4294967295).", + ); + } +} diff --git a/shared/tlv/proto_meta.rs b/shared/tlv/proto_meta.rs new file mode 100644 index 00000000..afe3d61a --- /dev/null +++ b/shared/tlv/proto_meta.rs @@ -0,0 +1,2387 @@ +// Copyright 2026 Google LLC +// +// 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. + +use descriptor_rust_proto::DescriptorProtoView; +use descriptor_rust_proto::FieldDescriptorProtoView; +use openprot_proto_reader::ProtoReader; +use openprot_proto_reader::Record; +use openprot_proto_reflect::TypeExt; +use protobuf::Parse; +use protobuf::Serialize; + +const TLV_FOURCC_OPTION_NUMBER: u32 = 12345; +const BIT_PACKED_OPTION_NUMBER: u32 = 12346; + +const OFFSET_OPTION_NUMBER: u32 = 12345; +const TYPE_OPTION_NUMBER: u32 = 12346; +const BIT_RANGE_OPTION_NUMBER: u32 = 12347; +const EXTRA_DATA_OPTION_NUMBER: u32 = 12348; + +pub use ::protobuf::__internal::runtime::__unstable::DescriptorInfo; +/// Re-export the Type enum so clients of proto_meta can easily use it. +pub use descriptor_rust_proto::field_descriptor_proto::Type; + +/// Recursively parses a `DescriptorInfo` dependency tree into a strongly-typed `FileDescriptorSet`. +pub fn collect_descriptor_set( + info: &DescriptorInfo, +) -> Option { + fn inner( + info: &DescriptorInfo, + set: &mut descriptor_rust_proto::FileDescriptorSet, + visited: &mut std::collections::HashSet<*const DescriptorInfo>, + ) -> Option<()> { + let ptr = info as *const _; + if !visited.insert(ptr) { + return Some(()); + } + + let file_proto = descriptor_rust_proto::FileDescriptorProto::parse(info.descriptor).ok()?; + set.file_mut().push(file_proto); + + for dep in info.deps { + inner(dep, set, visited)?; + } + Some(()) + } + + let mut set = descriptor_rust_proto::FileDescriptorSet::new(); + let mut visited = std::collections::HashSet::new(); + inner(info, &mut set, &mut visited)?; + Some(set) +} + +pub trait NameStrExt { + fn name_str(&self) -> &str; +} + +impl<'a> NameStrExt for FieldDescriptorProtoView<'a> { + fn name_str(&self) -> &str { + self.name().to_str().unwrap_or("") + } +} + +impl<'a> NameStrExt for DescriptorProtoView<'a> { + fn name_str(&self) -> &str { + self.name().to_str().unwrap_or("") + } +} + +pub trait DescriptorProtoExt<'a> { + fn find_field(&self, field_number: u32) -> Option>; +} + +impl<'a> DescriptorProtoExt<'a> for DescriptorProtoView<'a> { + fn find_field(&self, field_number: u32) -> Option> { + self.field() + .into_iter() + .find(|f| f.number() as u32 == field_number) + } +} + +/// The backing integer type of a bit-packed message. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum BackingType { + U8, + U16, + U32, +} + +impl BackingType { + /// Returns the size in bytes of the backing type. + pub fn size(&self) -> usize { + match self { + Self::U8 => 1, + Self::U16 => 2, + Self::U32 => 4, + } + } + + /// Returns the width in bits of the backing type. + pub fn bits(&self) -> usize { + self.size() * 8 + } +} + +/// A strongly-typed sub-field bit layout within a bit-packed message. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct BitPackedField { + /// The bit offset within the bit-packed message. + pub bit_offset: usize, + /// The bit length of the field. + pub bit_len: usize, +} + +/// The specific kind of layout violation that occurred. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum TlvLayoutErrorKind { + /// The field has an invalid type option value. + InvalidTypeOption { + /// The invalid type option string specified. + value: String, + }, + /// The packable primitive field is missing a custom offset option. + MissingOffset { + /// The Protobuf type of the field. + kind: Type, + }, + /// The string or bytes field has a custom offset but is missing an explicit type option. + MissingTypeOption { + /// The Protobuf type of the field. + kind: Type, + /// The custom offset specified on the field. + offset: usize, + }, + /// The field type is not supported for TLV layout (only packable primitives, inline strings/bytes, and nested messages are supported). + UnsupportedFieldType { + /// The Protobuf type of the field. + kind: Type, + }, + /// The field's custom offset is not naturally aligned to its size/alignment. + UnalignedOffset { + /// The offset specified. + offset: usize, + /// The alignment/size of the field. + size: usize, + }, + /// A field in a bit-packed message is missing the required `bit_range` option. + MissingBitRange, + /// A field in a bit-packed message specified a custom `offset` or `type` (not allowed, packing is sequential). + OffsetOrTypeInBitPacked, + /// The `bit_range` option was specified on a non-packable field type. + BitRangeOnNonPackableType { + /// The Protobuf type of the field. + kind: Type, + }, + /// The `bit_range` option has an invalid format (must be "[high:low]"). + InvalidBitRangeFormat { + /// The invalid value specified. + value: String, + }, + /// The `bit_range` option has high bit less than low bit. + InvalidBitRangeOrder { + /// The high bit specified. + high: usize, + /// The low bit specified. + low: usize, + }, + /// The `bit_range` high bit is out of bounds for the backing register width. + BitRangeOutOfBounds { + /// The high bit specified. + high: usize, + /// The maximum bits allowed by the backing type. + max_bits: usize, + }, + /// Two or more fields overlap in their bit ranges. + OverlappingBits { + /// The name of the first field. + field_a: String, + /// The name of the second field. + field_b: String, + /// The bit range of the first field. + range_a: std::ops::RangeInclusive, + /// The bit range of the second field. + range_b: std::ops::RangeInclusive, + }, + /// The `bit_packed` option has an invalid string value (must be "u8", "u16", or "u32"). + InvalidBitPackedType { + /// The invalid value specified. + value: String, + }, + /// A nested message field has a custom `offset` but its type is not packable inline. + NestedMessageNotPackable { + /// The name of the nested message type. + type_name: String, + }, + /// A bit-packed nested message field is missing a custom `offset` option (cannot be serialized as a dynamic sub-TLV). + BitPackedCannotBeTlv { + /// The name of the nested message type. + type_name: String, + }, + /// A bit-packed message specified a `tlv_fourcc` option (not allowed, bit-packed messages are raw registers with no TLV header). + BitPackedCannotHaveFourcc, + /// A nested message type descriptor was not found in the descriptor set. + NestedMessageNotFound { + /// The name of the nested message type. + type_name: String, + }, + /// Two or more fields overlap in their byte ranges in the fixed struct. + OverlappingFields { + /// The name of the first field. + field_a: String, + /// The name of the second field. + field_b: String, + /// The byte range of the first field. + range_a: std::ops::Range, + /// The byte range of the second field. + range_b: std::ops::Range, + }, + /// The string or bytes field has an explicit size override of 0 (must be greater than 0). + ZeroSizeStringOrBytes, + /// Standalone boolean fields are not supported in standard TLV structures. + StandaloneBoolNotSupported, + /// The signedness of the Protobuf field type does not match the signedness of the custom TLV type option. + SignednessMismatch { + /// The Protobuf type of the field. + proto_type: Type, + /// The custom type option string specified. + value: String, + }, + /// The float/integer nature of the Protobuf field type does not match the custom TLV type option. + FloatIntegerMismatch { + /// The Protobuf type of the field. + proto_type: Type, + /// The custom type option string specified. + value: String, + }, + /// A repeated field is not allowed to have a custom `offset` option. + RepeatedFieldCannotHaveOffset, + /// A field inside a `oneof` is not allowed to have a custom `offset` option. + OneofFieldCannotHaveOffset, + /// A nested message acting as a dynamic sub-TLV is missing the `tlv_fourcc` option. + MissingTlvFourcc { + /// The name of the nested message type. + type_name: String, + }, + /// Two or more sub-TLV fields in the same message share the same tlv_fourcc tag. + DuplicateTlvTag { + /// The duplicated 4-character tag. + tag: [u8; 4], + /// The name of the first field. + field_a: String, + /// The name of the second field. + field_b: String, + }, + /// A circular dependency was detected in the inline nested message layout. + CircularDependency { + /// The name of the message that triggered the cycle. + type_name: String, + }, + /// A field with `extra_data` option specified an explicit `offset` option. + ExtraDataWithOffset, + /// A field with `extra_data` option is missing an explicit `type` option. + ExtraDataMissingTypeOption, + /// A field with `extra_data` option specified an unsupported `type` option value. + ExtraDataUnsupportedType { + /// The invalid type option string specified. + value: String, + }, + /// A field with `extra_data` option is not a string or bytes field. + ExtraDataOnNonStringOrBytes { + /// The Protobuf type of the field. + kind: Type, + }, + /// Multiple fields in the same message specified the `extra_data` option. + MultipleExtraDataFields, + /// A message contains both an `extra_data` field and dynamic sub-TLVs. + ExtraDataWithSubTlvs, + /// A repeated field specified the `extra_data` option. + RepeatedFieldCannotHaveExtraData, + /// A field inside a `oneof` specified the `extra_data` option. + OneofFieldCannotHaveExtraData, +} + +struct VerilogRange(std::ops::RangeInclusive); + +impl std::fmt::Display for VerilogRange { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let (low, high) = (*self.0.start(), *self.0.end()); + if high == low { + write!(f, "{}", low) + } else { + write!(f, "{}:{}", high, low) + } + } +} + +impl std::fmt::Display for TlvLayoutErrorKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::InvalidTypeOption { value } => { + write!( + f, + "explicit type option must be a valid Rust type (got '{}').", + value + ) + } + Self::MissingOffset { kind } => { + write!( + f, + "primitive field of type '{:?}' is missing a custom 'offset' option. All primitive fields in a TLV-serializable message must be packed in the fixed struct.", + kind + ) + } + Self::MissingTypeOption { kind, offset } => { + write!( + f, + "field of type '{:?}' has a custom offset ({}) but is missing an explicit 'type' option. Inline strings or byte arrays in the fixed struct must specify an explicit type.", + kind, offset + ) + } + Self::UnsupportedFieldType { kind } => { + write!( + f, + "Protobuf type '{:?}' is not supported for TLV layout (only packable primitives, inline strings/bytes, and nested sub-messages are supported).", + kind + ) + } + Self::UnalignedOffset { offset, size } => { + write!( + f, + "offset of {} is not naturally aligned to its size of {} bytes (offset must be a multiple of size).", + offset, size + ) + } + Self::MissingBitRange => { + write!( + f, + "field in a bit-packed message is missing the required 'bit_range' option." + ) + } + Self::OffsetOrTypeInBitPacked => { + write!( + f, + "field in a bit-packed message is not allowed to specify 'offset' or 'type' options; use the 'bit_range' option to specify its position." + ) + } + Self::BitRangeOnNonPackableType { kind } => { + write!( + f, + "bit_range option is only supported on packable primitive integer, enum, or boolean types (got '{:?}').", + kind + ) + } + Self::InvalidBitRangeFormat { value } => { + write!( + f, + "invalid bit_range format: '{}' (must be Verilog notation like 'high:low').", + value + ) + } + Self::InvalidBitRangeOrder { high, low } => { + write!( + f, + "invalid bit_range order '{}:{}': high bit must be greater than or equal to low bit.", + high, low + ) + } + Self::BitRangeOutOfBounds { high, max_bits } => { + write!( + f, + "bit_range high bit {} is out of bounds for the backing register width of {} bits.", + high, max_bits + ) + } + Self::OverlappingBits { + field_a, + field_b, + range_a, + range_b, + } => { + write!( + f, + "field '{}' (bits {}) overlaps with field '{}' (bits {}) in the bit-packed register.", + field_a, + VerilogRange(range_a.clone()), + field_b, + VerilogRange(range_b.clone()) + ) + } + Self::InvalidBitPackedType { value } => { + write!( + f, + "invalid value for 'bit_packed' option: '{}' (must be 'u8', 'u16', or 'u32').", + value + ) + } + Self::NestedMessageNotPackable { type_name } => { + write!( + f, + "nested message field has a custom offset but its type '{}' is not packable inline (it must be bit-packed or a standard struct with no dynamic sub-TLVs).", + type_name + ) + } + Self::BitPackedCannotBeTlv { type_name } => { + write!( + f, + "bit-packed message of type '{}' cannot be serialized as a dynamic sub-TLV; it must be packed inline inside the fixed struct using a custom 'offset' option.", + type_name + ) + } + Self::BitPackedCannotHaveFourcc => { + write!( + f, + "message is bit-packed and cannot specify a 'tlv_fourcc' option; bit-packed messages are raw registers and cannot be serialized as TLV blocks." + ) + } + Self::MissingTlvFourcc { type_name } => { + write!( + f, + "nested message '{}' acts as a dynamic sub-TLV but is missing a 'tlv_fourcc' option; sub-TLVs must have a unique 4-character tag.", + type_name + ) + } + Self::DuplicateTlvTag { + tag, + field_a, + field_b, + } => { + let tag_str = String::from_utf8_lossy(tag).into_owned(); + write!( + f, + "sub-TLV fields '{}' and '{}' share the same tlv_fourcc tag '{}'; all sub-TLV fields within a message must have unique tags to prevent decoding ambiguity.", + field_a, field_b, tag_str + ) + } + Self::CircularDependency { type_name } => { + write!( + f, + "circular dependency detected: message '{}' directly or indirectly contains itself inline, which requires infinite struct size.", + type_name + ) + } + Self::NestedMessageNotFound { type_name } => { + write!( + f, + "nested message type descriptor for '{}' was not found in the descriptor set.", + type_name + ) + } + Self::OverlappingFields { + field_a, + field_b, + range_a, + range_b, + } => { + write!( + f, + "field '{}' (bytes {}..{}) overlaps with field '{}' (bytes {}..{}) in the fixed struct.", + field_a, range_a.start, range_a.end, field_b, range_b.start, range_b.end + ) + } + Self::ZeroSizeStringOrBytes => { + write!( + f, + "explicit size option for inline string or bytes fields must be greater than 0." + ) + } + Self::StandaloneBoolNotSupported => { + write!( + f, + "standalone boolean fields are not supported because they cannot be safely decoded via zero-copy. Use a custom 'type' option to map the field to an integer type (e.g. `[(openprot.tlv.type) = \"u8\"]`), or place the field inside a bit-packed message (using `option (openprot.tlv.bit_packed) = ...;`)." + ) + } + Self::SignednessMismatch { proto_type, value } => { + let proto_info = proto_type.primitive_info().unwrap(); + let proto_signed_str = if proto_info.is_signed { + "signed" + } else { + "unsigned" + }; + + let parsed_type = PackedType::parse(value).unwrap(); + let packed_signed_str = if parsed_type.is_signed() { + "signed" + } else { + "unsigned" + }; + + let suggestion = if proto_info.is_signed { + "Signed Protobuf fields must be mapped to signed Rust types (e.g. i8, i16, i32, i64)." + } else { + "Unsigned Protobuf fields must be mapped to unsigned Rust types (e.g. u8, u16, u32, u64)." + }; + + write!( + f, + "signedness mismatch: Protobuf field type '{:?}' ({}) does not match the signedness of the custom TLV type '{}' ({}). {}", + proto_type, proto_signed_str, value, packed_signed_str, suggestion + ) + } + Self::FloatIntegerMismatch { proto_type, value } => { + let proto_info = proto_type.primitive_info().unwrap(); + let proto_str = if proto_info.is_float { + "floating-point" + } else { + "integer" + }; + + let parsed_type = PackedType::parse(value).unwrap(); + let packed_str = if parsed_type.is_float() { + "floating-point" + } else { + "integer" + }; + + let suggestion = if proto_info.is_float { + "Floating-point Protobuf fields must be mapped to floating-point Rust types (f32, f64)." + } else { + "Integer Protobuf fields must be mapped to integer Rust types (e.g. i8, u8, i16, u16, i32, u32, i64, u64)." + }; + + write!( + f, + "type mismatch: Protobuf field type '{:?}' ({}) does not match the custom TLV type '{}' ({}). {}", + proto_type, proto_str, value, packed_str, suggestion + ) + } + Self::RepeatedFieldCannotHaveOffset => { + write!( + f, + "repeated fields are not allowed to specify a custom 'offset' option because their size is dynamic. They must be serialized as dynamic sub-TLVs (omit the 'offset' option)." + ) + } + Self::OneofFieldCannotHaveOffset => { + write!( + f, + "fields inside a 'oneof' are not allowed to specify a custom 'offset' option because they are mutually exclusive. They must be serialized as dynamic sub-TLVs (omit the 'offset' option)." + ) + } + Self::ExtraDataWithOffset => { + write!( + f, + "field with 'extra_data' option cannot specify a custom 'offset' option." + ) + } + Self::ExtraDataMissingTypeOption => { + write!( + f, + "field with 'extra_data' option must explicitly specify a 'type' option (e.g., `[(openprot.tlv.type) = \"[u8]\"]`)." + ) + } + Self::ExtraDataUnsupportedType { value } => { + write!( + f, + "'extra_data' option currently only supports type '[u8]' (got '{}').", + value + ) + } + Self::ExtraDataOnNonStringOrBytes { kind } => { + write!( + f, + "'extra_data' option is only supported on 'string' or 'bytes' fields (got '{:?}').", + kind + ) + } + Self::MultipleExtraDataFields => { + write!( + f, + "message cannot have more than one field with 'extra_data' option." + ) + } + Self::ExtraDataWithSubTlvs => { + write!( + f, + "message with an 'extra_data' field cannot also contain dynamic sub-TLVs." + ) + } + Self::RepeatedFieldCannotHaveExtraData => { + write!(f, "field with 'extra_data' option cannot be 'repeated'.") + } + Self::OneofFieldCannotHaveExtraData => { + write!( + f, + "field with 'extra_data' option cannot be inside a 'oneof'." + ) + } + } + } +} + +/// A structured, strongly-typed layout error. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TlvLayoutError { + /// The name of the message where the error occurred. + pub message_name: String, + /// The name of the field where the error occurred, if applicable. + pub field_name: Option, + /// The specific kind of layout violation. + pub kind: TlvLayoutErrorKind, +} + +impl std::fmt::Display for TlvLayoutError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if let Some(field_name) = &self.field_name { + write!( + f, + "Invalid TLV layout on field '{}' of message '{}': {}", + field_name, self.message_name, self.kind + ) + } else { + write!( + f, + "Invalid TLV layout in message '{}': {}", + self.message_name, self.kind + ) + } + } +} + +impl std::error::Error for TlvLayoutError {} + +/// The layout and serialization strategy for a field in the TLV representation. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum FieldLayout { + /// The field is packed into the fixed struct at a specific offset and size in bytes. + Fixed { + /// The byte offset of the field in the fixed TLV struct. + offset: usize, + /// The resolved packed type representation of the field. + packed_type: PackedType, + }, + /// The field is serialized as a dynamically appended sub-TLV. + Tlv { + /// The underlying Protobuf type of the field. + kind: Type, + /// The 4-character TLV tag. + tag: [u8; 4], + }, + /// The field is stored as the unpadded raw extra data payload of the TLV record. + ExtraData, +} + +/// Fully resolved and validated TLV layout for a message descriptor. +#[derive(Debug, Clone)] +pub enum TlvLayout { + /// A standard TLV message with a byte-aligned fixed struct and optional dynamic sub-TLVs. + Standard { + /// Mapping of field numbers to their resolved `FieldLayout`s. + fields: std::collections::HashMap, + /// The total resolved size in bytes of the fixed struct portion of the message. + fixed_struct_size: usize, + }, + /// A bit-packed message where all fields are packed sequentially at the bit level. + BitPacked { + /// Mapping of field numbers to their resolved `BitPackedField` metadata. + fields: std::collections::HashMap, + /// The backing integer type of the bit-packed message. + backing: BackingType, + }, +} + +impl TlvLayout { + /// Returns the total resolved size in bytes of the fixed struct/body. + pub fn fixed_struct_size(&self) -> usize { + match self { + Self::Standard { + fixed_struct_size, .. + } => *fixed_struct_size, + Self::BitPacked { backing, .. } => backing.size(), + } + } +} + +/// Extension trait providing convenient access to TLV custom options on message descriptors. +pub trait DescriptorProtoTlvExt { + /// Returns the `tlv_fourcc` custom option value of this message as a 4-byte array, if present. + fn tlv_fourcc(&self) -> Option<[u8; 4]>; + + /// Returns the parsed `bit_packed` custom option of this message, if present. + #[allow(clippy::result_large_err)] + fn bit_packed(&self) -> Result, TlvLayoutError>; + + /// Resolves and validates the entire `TlvLayout` for this message descriptor. + #[allow(clippy::result_large_err)] + fn tlv_layout( + &self, + desc_set: &descriptor_rust_proto::FileDescriptorSet, + ) -> Result; + + /// Resolves and validates the entire `TlvLayout` for this message descriptor using a custom name path. + #[allow(clippy::result_large_err)] + fn tlv_layout_with_name( + &self, + full_name: &str, + desc_set: &descriptor_rust_proto::FileDescriptorSet, + ) -> Result; +} + +impl<'a> DescriptorProtoTlvExt for DescriptorProtoView<'a> { + fn tlv_fourcc(&self) -> Option<[u8; 4]> { + let opt_bytes = self.options().serialize().ok()?; + let reader = ProtoReader::new(&opt_bytes); + match reader.find_field(TLV_FOURCC_OPTION_NUMBER) { + Some(Record::LengthDelimited(bytes)) => { + if bytes.len() == 4 { + let mut tag = [0u8; 4]; + tag.copy_from_slice(bytes); + Some(tag) + } else { + None + } + } + _ => None, + } + } + + #[allow(clippy::result_large_err)] + fn bit_packed(&self) -> Result, TlvLayoutError> { + let opt_bytes = match self.options().serialize() { + Ok(bytes) => bytes, + Err(_) => return Ok(None), + }; + let reader = ProtoReader::new(&opt_bytes); + match reader.find_field(BIT_PACKED_OPTION_NUMBER) { + Some(Record::LengthDelimited(bytes)) => { + if bytes == b"u8" { + Ok(Some(BackingType::U8)) + } else if bytes == b"u16" { + Ok(Some(BackingType::U16)) + } else if bytes == b"u32" { + Ok(Some(BackingType::U32)) + } else { + let invalid_str = String::from_utf8_lossy(bytes).into_owned(); + Err(TlvLayoutError { + message_name: self.name_str().to_string(), + field_name: None, + kind: TlvLayoutErrorKind::InvalidBitPackedType { value: invalid_str }, + }) + } + } + _ => Ok(None), + } + } + + fn tlv_layout( + &self, + desc_set: &descriptor_rust_proto::FileDescriptorSet, + ) -> Result { + self.tlv_layout_with_name(self.name_str(), desc_set) + } + + fn tlv_layout_with_name( + &self, + full_name: &str, + desc_set: &descriptor_rust_proto::FileDescriptorSet, + ) -> Result { + let mut active_resolves = std::collections::HashSet::new(); + tlv_layout_rec(self, full_name, desc_set, &mut active_resolves) + } +} + +const RAW_TYPE_OPTION_NUMBER: u32 = 12345; + +/// Extension trait providing convenient access to TLV custom options on enum descriptors. +pub trait EnumDescriptorProtoTlvExt { + /// Returns the `raw_type` custom option value of this enum, if present. + fn raw_type(&self) -> Option; +} + +impl<'a> EnumDescriptorProtoTlvExt for descriptor_rust_proto::EnumDescriptorProtoView<'a> { + fn raw_type(&self) -> Option { + let opt_bytes = self.options().serialize().ok()?; + let reader = ProtoReader::new(&opt_bytes); + match reader.find_field(RAW_TYPE_OPTION_NUMBER) { + Some(Record::LengthDelimited(bytes)) => String::from_utf8(bytes.to_vec()).ok(), + _ => None, + } + } +} + +#[allow(clippy::result_large_err)] +fn tlv_layout_rec( + msg: &DescriptorProtoView<'_>, + full_name: &str, + desc_set: &descriptor_rust_proto::FileDescriptorSet, + active_resolves: &mut std::collections::HashSet, +) -> Result { + let msg_name = msg.name_str().to_string(); + if !active_resolves.insert(full_name.to_string()) { + return Err(TlvLayoutError { + message_name: msg_name.clone(), + field_name: None, + kind: TlvLayoutErrorKind::CircularDependency { + type_name: full_name.to_string(), + }, + }); + } + + let bit_packed_opt = msg.bit_packed()?; + + let res = if let Some(backing) = bit_packed_opt { + // Validate that a bit-packed message does NOT have a tlv_fourcc option! + if msg.tlv_fourcc().is_some() { + return Err(TlvLayoutError { + message_name: msg_name.clone(), + field_name: None, + kind: TlvLayoutErrorKind::BitPackedCannotHaveFourcc, + }); + } + + let mod_fields = &backing; // Wait, to avoid shadowing + let mut fields = std::collections::HashMap::new(); + let mut bit_ranges_collected = Vec::new(); + + for field in msg.field() { + let field_number = field.number() as u32; + let kind = field.r#type(); + let field_name = field.name_str().to_string(); + + // Validate no offset or type options on bit-packed fields + if field.offset().is_some() || field.type_option().is_some() { + return Err(TlvLayoutError { + message_name: msg_name.clone(), + field_name: Some(field_name), + kind: TlvLayoutErrorKind::OffsetOrTypeInBitPacked, + }); + } + + // Validate type is a packable primitive (integers, enums, and booleans only!) + if kind.primitive_info().is_none_or(|info| info.is_float) { + return Err(TlvLayoutError { + message_name: msg_name.clone(), + field_name: Some(field_name), + kind: TlvLayoutErrorKind::BitRangeOnNonPackableType { kind }, + }); + } + + let bit_range = match field.bit_range() { + Some(r) => r, + None => { + return Err(TlvLayoutError { + message_name: msg_name.clone(), + field_name: Some(field_name), + kind: TlvLayoutErrorKind::MissingBitRange, + }); + } + }; + + let Some(range) = parse_verilog_bit_range(&bit_range) else { + return Err(TlvLayoutError { + message_name: msg_name.clone(), + field_name: Some(field_name), + kind: TlvLayoutErrorKind::InvalidBitRangeFormat { value: bit_range }, + }); + }; + + let (high, low) = (*range.end(), *range.start()); + if high < low { + return Err(TlvLayoutError { + message_name: msg_name.clone(), + field_name: Some(field_name), + kind: TlvLayoutErrorKind::InvalidBitRangeOrder { high, low }, + }); + } + + if high >= mod_fields.bits() { + return Err(TlvLayoutError { + message_name: msg_name.clone(), + field_name: Some(field_name), + kind: TlvLayoutErrorKind::BitRangeOutOfBounds { + high, + max_bits: mod_fields.bits(), + }, + }); + } + + let bit_len = high - low + 1; + fields.insert( + field_number, + BitPackedField { + bit_offset: low, + bit_len, + }, + ); + + bit_ranges_collected.push((range, field_name)); + } + + // Validate bit-level overlapping fields + bit_ranges_collected.sort_by_key(|r| *r.0.start()); + for i in 1..bit_ranges_collected.len() { + let prev = &bit_ranges_collected[i - 1]; + let curr = &bit_ranges_collected[i]; + + if *curr.0.start() <= *prev.0.end() { + return Err(TlvLayoutError { + message_name: msg_name.clone(), + field_name: None, + kind: TlvLayoutErrorKind::OverlappingBits { + field_a: prev.1.clone(), + field_b: curr.1.clone(), + range_a: prev.0.clone(), + range_b: curr.0.clone(), + }, + }); + } + } + + Ok(TlvLayout::BitPacked { fields, backing }) + } else { + // --- 2. Resolve Standard Message Layout --- + let mut fields = std::collections::HashMap::new(); + let mut fixed_fields_ranges = Vec::new(); + let mut max_offset_plus_size = 0; + let mut tag_to_field = std::collections::HashMap::new(); + + let mut extra_data_count = 0; + let mut tlv_count = 0; + + for field in msg.field() { + let field_number = field.number() as u32; + let layout = + field_tlv_layout_rec(&field, desc_set, active_resolves).map_err(|kind| { + TlvLayoutError { + message_name: msg_name.clone(), + field_name: Some(field.name_str().to_string()), + kind, + } + })?; + + if matches!(layout, FieldLayout::ExtraData) { + extra_data_count += 1; + } + if matches!(layout, FieldLayout::Tlv { .. }) { + tlv_count += 1; + } + + if let FieldLayout::Tlv { tag, .. } = &layout { + let field_name = field.name_str().to_string(); + if let Some(prev_field) = tag_to_field.insert(*tag, field_name.clone()) { + return Err(TlvLayoutError { + message_name: msg_name.clone(), + field_name: None, + kind: TlvLayoutErrorKind::DuplicateTlvTag { + tag: *tag, + field_a: prev_field, + field_b: field_name, + }, + }); + } + } + + if let FieldLayout::Fixed { + offset, + packed_type, + } = &layout + { + let size = packed_type.size(); + fixed_fields_ranges.push((*offset..*offset + size, field.name_str().to_string())); + + if *offset + size > max_offset_plus_size { + max_offset_plus_size = *offset + size; + } + } + fields.insert(field_number, layout); + } + + if extra_data_count > 1 { + return Err(TlvLayoutError { + message_name: msg_name.clone(), + field_name: None, + kind: TlvLayoutErrorKind::MultipleExtraDataFields, + }); + } + if extra_data_count > 0 && tlv_count > 0 { + return Err(TlvLayoutError { + message_name: msg_name.clone(), + field_name: None, + kind: TlvLayoutErrorKind::ExtraDataWithSubTlvs, + }); + } + + // Validate overlapping fields + fixed_fields_ranges.sort_by_key(|r| r.0.start); + for i in 1..fixed_fields_ranges.len() { + let prev = &fixed_fields_ranges[i - 1]; + let curr = &fixed_fields_ranges[i]; + + if curr.0.start < prev.0.end { + return Err(TlvLayoutError { + message_name: msg_name.clone(), + field_name: None, + kind: TlvLayoutErrorKind::OverlappingFields { + field_a: prev.1.clone(), + field_b: curr.1.clone(), + range_a: prev.0.clone(), + range_b: curr.0.clone(), + }, + }); + } + } + + let fixed_struct_size = (max_offset_plus_size + 3) & !3; + Ok(TlvLayout::Standard { + fields, + fixed_struct_size, + }) + }; + + active_resolves.remove(full_name); + res +} + +/// Helper to parse Verilog bit range format like "7:0" (multi-bit) or "0" (single-bit) into a RangeInclusive. +fn parse_verilog_bit_range(s: &str) -> Option> { + let s = s.trim(); + if s.contains(':') { + let mut parts = s.split(':'); + let high_str = parts.next()?; + let low_str = parts.next()?; + if parts.next().is_some() { + return None; + } + let high = high_str.trim().parse::().ok()?; + let low = low_str.trim().parse::().ok()?; + Some(low..=high) + } else { + let bit = s.parse::().ok()?; + Some(bit..=bit) + } +} + +/// The primitive types supported in a packed struct. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PackedPrimitiveType { + U8, + U16, + U32, + U64, + I8, + I16, + I32, + I64, + F32, + F64, + Bool, +} + +impl PackedPrimitiveType { + /// Parses a primitive type from a string. + pub fn parse(s: &str) -> Option { + match s.trim() { + "u8" => Some(Self::U8), + "u16" => Some(Self::U16), + "u32" => Some(Self::U32), + "u64" => Some(Self::U64), + "i8" => Some(Self::I8), + "i16" => Some(Self::I16), + "i32" => Some(Self::I32), + "i64" => Some(Self::I64), + "f32" => Some(Self::F32), + "f64" => Some(Self::F64), + "bool" => Some(Self::Bool), + _ => None, + } + } + + /// Returns the size in bytes of the primitive type. + pub fn size(&self) -> usize { + match self { + Self::U8 | Self::I8 | Self::Bool => 1, + Self::U16 | Self::I16 => 2, + Self::U32 | Self::I32 | Self::F32 => 4, + Self::U64 | Self::I64 | Self::F64 => 8, + } + } + + /// Returns whether the primitive type is signed. + pub fn is_signed(&self) -> bool { + match self { + Self::I8 | Self::I16 | Self::I32 | Self::I64 | Self::F32 | Self::F64 => true, + Self::U8 | Self::U16 | Self::U32 | Self::U64 | Self::Bool => false, + } + } + + /// Returns true if this primitive is a floating point type. + pub fn is_float(&self) -> bool { + matches!(self, Self::F32 | Self::F64) + } + + /// Returns the alignment in bytes of the primitive type. + pub fn alignment(&self) -> usize { + // 64-bit primitives are mapped to 4-aligned wrapper types in codegen, + // so their layout alignment requirement is at most 4 bytes. + std::cmp::min(self.size(), 4) + } + + /// Maps a Protobuf Type to its default PackedPrimitiveType, if supported. + pub fn from_proto_type(kind: Type) -> Option { + let info = kind.primitive_info()?; + match info.size { + 1 => { + if kind == Type::Bool { + Some(Self::Bool) + } else if info.is_signed { + Some(Self::I8) + } else { + Some(Self::U8) + } + } + 2 => { + if info.is_signed { + Some(Self::I16) + } else { + Some(Self::U16) + } + } + 4 => { + if kind == Type::Float { + Some(Self::F32) + } else if info.is_signed { + Some(Self::I32) + } else { + Some(Self::U32) + } + } + 8 => { + if kind == Type::Double { + Some(Self::F64) + } else if info.is_signed { + Some(Self::I64) + } else { + Some(Self::U64) + } + } + _ => None, + } + } +} + +impl std::fmt::Display for PackedPrimitiveType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let name = match self { + Self::U8 => "u8", + Self::U16 => "u16", + Self::U32 => "u32", + Self::U64 => "u64", + Self::I8 => "i8", + Self::I16 => "i16", + Self::I32 => "i32", + Self::I64 => "i64", + Self::F32 => "f32", + Self::F64 => "f64", + Self::Bool => "bool", + }; + write!(f, "{}", name) + } +} + +/// The fully resolved packed type representation of a field. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PackedType { + Primitive(PackedPrimitiveType), + Array { + elem: PackedPrimitiveType, + len: usize, + }, + Struct { + name: String, + size: usize, + }, +} + +impl PackedType { + /// Parses a PackedType from a string (e.g. "u32", "[u8; 16]"). + pub fn parse(s: &str) -> Option { + let s = s.trim(); + if s.starts_with('[') && s.ends_with(']') { + let inner = &s[1..s.len() - 1]; + let mut parts = inner.split(';'); + let elem_str = parts.next()?.trim(); + let len_str = parts.next()?.trim(); + if parts.next().is_some() { + return None; + } + let elem = PackedPrimitiveType::parse(elem_str)?; + let len = len_str.parse::().ok()?; + Some(Self::Array { elem, len }) + } else { + let prim = PackedPrimitiveType::parse(s)?; + Some(Self::Primitive(prim)) + } + } + + /// Returns the total size in bytes of this packed type. + pub fn size(&self) -> usize { + match self { + Self::Primitive(prim) => prim.size(), + Self::Array { elem, len } => elem.size() * len, + Self::Struct { size, .. } => *size, + } + } + + /// Returns the alignment requirement in bytes of this packed type. + pub fn alignment(&self) -> usize { + match self { + Self::Primitive(prim) => prim.alignment(), + Self::Array { elem, .. } => elem.alignment(), + Self::Struct { .. } => 4, + } + } + + /// Returns true if this type represents a floating point primitive. + pub fn is_float(&self) -> bool { + match self { + Self::Primitive(prim) => prim.is_float(), + _ => false, + } + } + + /// Returns true if this type represents a signed primitive. + pub fn is_signed(&self) -> bool { + match self { + Self::Primitive(prim) => prim.is_signed(), + _ => false, + } + } + + /// Maps a Protobuf Type to its default PackedType, if supported. + pub fn from_proto_type(kind: Type) -> Option { + PackedPrimitiveType::from_proto_type(kind).map(Self::Primitive) + } +} + +impl std::fmt::Display for PackedType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Primitive(prim) => write!(f, "{}", prim), + Self::Array { elem, len } => write!(f, "[{}; {}]", elem, len), + Self::Struct { name, .. } => write!(f, "{}", name), + } + } +} + +/// Extension trait providing convenient access to TLV custom options on field descriptors. +pub trait FieldDescriptorProtoTlvExt { + /// Returns the `offset` custom option value of this field, if present. + fn offset(&self) -> Option; + + /// Returns the `type` custom option value of this field as a String, if present. + fn type_option(&self) -> Option; + + /// Returns the `bit_range` custom option value of this field as a String, if present. + fn bit_range(&self) -> Option; + + /// Returns true if the `extra_data` custom option is set to true on this field. + fn extra_data(&self) -> bool; + + /// Resolves and validates the `FieldLayout` for this field. + fn tlv_layout( + &self, + desc_set: &descriptor_rust_proto::FileDescriptorSet, + ) -> Result; +} + +impl<'a> FieldDescriptorProtoTlvExt for FieldDescriptorProtoView<'a> { + fn offset(&self) -> Option { + let opt_bytes = self.options().serialize().ok()?; + let reader = ProtoReader::new(&opt_bytes); + match reader.find_field(OFFSET_OPTION_NUMBER) { + Some(Record::Varint(val)) => Some(val as u32), + _ => None, + } + } + + fn type_option(&self) -> Option { + let opt_bytes = self.options().serialize().ok()?; + let reader = ProtoReader::new(&opt_bytes); + match reader.find_field(TYPE_OPTION_NUMBER) { + Some(Record::LengthDelimited(bytes)) => String::from_utf8(bytes.to_vec()).ok(), + _ => None, + } + } + + fn bit_range(&self) -> Option { + let opt_bytes = self.options().serialize().ok()?; + let reader = ProtoReader::new(&opt_bytes); + match reader.find_field(BIT_RANGE_OPTION_NUMBER) { + Some(Record::LengthDelimited(bytes)) => String::from_utf8(bytes.to_vec()).ok(), + _ => None, + } + } + + fn extra_data(&self) -> bool { + let Ok(opt_bytes) = self.options().serialize() else { + return false; + }; + let reader = ProtoReader::new(&opt_bytes); + matches!(reader.find_field(EXTRA_DATA_OPTION_NUMBER), Some(Record::Varint(val)) if val != 0) + } + + fn tlv_layout( + &self, + desc_set: &descriptor_rust_proto::FileDescriptorSet, + ) -> Result { + let mut active_resolves = std::collections::HashSet::new(); + field_tlv_layout_rec(self, desc_set, &mut active_resolves) + } +} + +fn field_tlv_layout_rec( + field: &FieldDescriptorProtoView<'_>, + desc_set: &descriptor_rust_proto::FileDescriptorSet, + active_resolves: &mut std::collections::HashSet, +) -> Result { + use descriptor_rust_proto::field_descriptor_proto::Label; + + if field.extra_data() { + if field.offset().is_some() { + return Err(TlvLayoutErrorKind::ExtraDataWithOffset); + } + if field.label() == Label::Repeated { + return Err(TlvLayoutErrorKind::RepeatedFieldCannotHaveExtraData); + } + if field.has_oneof_index() { + return Err(TlvLayoutErrorKind::OneofFieldCannotHaveExtraData); + } + let kind = field.r#type(); + if !matches!(kind, Type::String | Type::Bytes) { + return Err(TlvLayoutErrorKind::ExtraDataOnNonStringOrBytes { kind }); + } + let Some(type_opt) = field.type_option() else { + return Err(TlvLayoutErrorKind::ExtraDataMissingTypeOption); + }; + if type_opt != "[u8]" { + return Err(TlvLayoutErrorKind::ExtraDataUnsupportedType { value: type_opt }); + } + return Ok(FieldLayout::ExtraData); + } + + if field.label() == Label::Repeated && field.offset().is_some() { + return Err(TlvLayoutErrorKind::RepeatedFieldCannotHaveOffset); + } + + if field.has_oneof_index() && field.offset().is_some() { + return Err(TlvLayoutErrorKind::OneofFieldCannotHaveOffset); + } + + let kind = field.r#type(); + + let explicit_type = field.type_option(); + let bit_range = field.bit_range(); + + if bit_range.is_some() && field.offset().is_some() { + return Err(TlvLayoutErrorKind::BitRangeOnNonPackableType { kind }); + } + + let offset = match field.offset() { + Some(o) => o as usize, + None => { + if kind.primitive_info().is_some() { + return Err(TlvLayoutErrorKind::MissingOffset { kind }); + } + + if !matches!(kind, Type::Message | Type::Group) { + return Err(TlvLayoutErrorKind::UnsupportedFieldType { kind }); + } + + // There is no offset, so this must be a TLV field. + + let type_name_raw = field.type_name().to_str().unwrap_or(""); + let type_name = type_name_raw.strip_prefix('.').unwrap_or(type_name_raw); + + let child_desc = find_message_descriptor(desc_set, type_name).ok_or_else(|| { + TlvLayoutErrorKind::NestedMessageNotFound { + type_name: type_name.to_string(), + } + })?; + + if child_desc.bit_packed().map_err(|e| e.kind)?.is_some() { + return Err(TlvLayoutErrorKind::BitPackedCannotBeTlv { + type_name: type_name.to_string(), + }); + } + + let tag = + child_desc + .tlv_fourcc() + .ok_or_else(|| TlvLayoutErrorKind::MissingTlvFourcc { + type_name: type_name.to_string(), + })?; + + return Ok(FieldLayout::Tlv { kind, tag }); + } + }; + + // Packable primitive types -> Valid Fixed layout! + if kind.primitive_info().is_some() { + let packed_type = if let Some(type_str) = explicit_type { + let Some(parsed_type) = PackedType::parse(&type_str) else { + return Err(TlvLayoutErrorKind::InvalidTypeOption { value: type_str }); + }; + // Ensure it is a primitive type (not an array) + if matches!(parsed_type, PackedType::Array { .. }) { + return Err(TlvLayoutErrorKind::InvalidTypeOption { value: type_str }); + } + + // Validate signedness matches! + let info = kind.primitive_info().unwrap(); + if info.is_signed != parsed_type.is_signed() { + return Err(TlvLayoutErrorKind::SignednessMismatch { + proto_type: kind, + value: type_str, + }); + } + + // Validate float/integer matches! + if info.is_float != parsed_type.is_float() { + return Err(TlvLayoutErrorKind::FloatIntegerMismatch { + proto_type: kind, + value: type_str, + }); + } + + parsed_type + } else if kind == descriptor_rust_proto::field_descriptor_proto::Type::Enum { + let type_name_raw = field.type_name().to_str().unwrap_or(""); + let type_name = type_name_raw.strip_prefix('.').unwrap_or(type_name_raw); + let raw_type_str = find_enum_descriptor(desc_set, type_name) + .and_then(|e| e.raw_type()) + .unwrap_or_else(|| "i32".to_string()); + + let Some(parsed_type) = PackedType::parse(&raw_type_str) else { + return Err(TlvLayoutErrorKind::InvalidTypeOption { + value: raw_type_str, + }); + }; + parsed_type + } else { + PackedType::from_proto_type(kind) + .ok_or(TlvLayoutErrorKind::UnsupportedFieldType { kind })? + }; + + if matches!( + packed_type, + PackedType::Primitive(PackedPrimitiveType::Bool) + ) { + return Err(TlvLayoutErrorKind::StandaloneBoolNotSupported); + } + + let align = packed_type.alignment(); + + if offset % align != 0 { + return Err(TlvLayoutErrorKind::UnalignedOffset { + offset, + size: align, + }); + } + + return Ok(FieldLayout::Fixed { + offset, + packed_type, + }); + } + + match kind { + Type::String | Type::Bytes => { + // String/bytes are allowed to be packed as fixed arrays + if let Some(type_str) = explicit_type { + let Some(parsed_type) = PackedType::parse(&type_str) else { + return Err(TlvLayoutErrorKind::InvalidTypeOption { value: type_str }); + }; + let size = parsed_type.size(); + if size == 0 { + return Err(TlvLayoutErrorKind::ZeroSizeStringOrBytes); + } + // String/bytes must be packed as an array type in the fixed struct + if !matches!(parsed_type, PackedType::Array { .. }) { + return Err(TlvLayoutErrorKind::InvalidTypeOption { value: type_str }); + } + + // Validate it is not a boolean array + if matches!( + parsed_type, + PackedType::Array { + elem: PackedPrimitiveType::Bool, + .. + } + ) { + return Err(TlvLayoutErrorKind::StandaloneBoolNotSupported); + } + let align = parsed_type.alignment(); + if offset % align != 0 { + return Err(TlvLayoutErrorKind::UnalignedOffset { + offset, + size: align, + }); + } + Ok(FieldLayout::Fixed { + offset, + packed_type: parsed_type, + }) + } else { + Err(TlvLayoutErrorKind::MissingTypeOption { kind, offset }) + } + } + + Type::Message | Type::Group => { + // Nested messages/groups are allowed to be packed as fixed + // data if they have no sub TLVs + let type_name_raw = field.type_name().to_str().unwrap_or(""); + let type_name = type_name_raw.strip_prefix('.').unwrap_or(type_name_raw); + + let child_desc = find_message_descriptor(desc_set, type_name).ok_or_else(|| { + TlvLayoutErrorKind::NestedMessageNotFound { + type_name: type_name.to_string(), + } + })?; + + let child_layout = tlv_layout_rec(&child_desc, type_name, desc_set, active_resolves) + .map_err(|e| e.kind)?; + + match child_layout { + TlvLayout::BitPacked { backing, .. } => { + let size = backing.size(); + + if size > 0 && offset % size != 0 { + return Err(TlvLayoutErrorKind::UnalignedOffset { offset, size }); + } + + let rust_type = get_rust_struct_name(type_name, desc_set); + Ok(FieldLayout::Fixed { + offset, + packed_type: PackedType::Struct { + name: rust_type, + size, + }, + }) + } + TlvLayout::Standard { + fields, + fixed_struct_size, + } => { + let has_dynamic_tlvs = fields + .values() + .any(|f| matches!(f, FieldLayout::Tlv { .. })); + if has_dynamic_tlvs { + return Err(TlvLayoutErrorKind::NestedMessageNotPackable { + type_name: type_name.to_string(), + }); + } + + let max_align = fields + .values() + .map(|f| match f { + FieldLayout::Fixed { packed_type, .. } => packed_type.alignment(), + _ => 1, + }) + .max() + .unwrap_or(1); + + if max_align > 0 && offset % max_align != 0 { + return Err(TlvLayoutErrorKind::UnalignedOffset { + offset, + size: max_align, + }); + } + + let rust_type = get_rust_struct_name(type_name, desc_set); + Ok(FieldLayout::Fixed { + offset, + packed_type: PackedType::Struct { + name: rust_type, + size: fixed_struct_size, + }, + }) + } + } + } + + _ => Err(TlvLayoutErrorKind::UnsupportedFieldType { kind }), + } +} + +fn normalize_name(s: &str) -> String { + s.chars() + .filter(|c| c.is_alphanumeric()) + .collect::() + .to_lowercase() +} + +fn extract_simple_name(type_name: &str) -> &str { + if let Some(pos) = type_name.rfind("::") { + &type_name[pos + 2..] + } else if let Some(pos) = type_name.rfind('.') { + &type_name[pos + 1..] + } else { + type_name + } +} + +pub fn get_rust_struct_name( + full_type_name: &str, + desc_set: &descriptor_rust_proto::FileDescriptorSet, +) -> String { + let mut best_package = ""; + for file in desc_set.file() { + let pkg = file.package().to_str().unwrap(); + let pkg_prefix = if pkg.is_empty() { + "".to_string() + } else { + format!("{}.", pkg) + }; + if full_type_name.starts_with(&pkg_prefix) { + if pkg.len() > best_package.len() { + best_package = pkg; + } + } + } + + let relative_name = if best_package.is_empty() { + full_type_name + } else { + full_type_name + .strip_prefix(&format!("{}.", best_package)) + .unwrap_or(full_type_name) + }; + + relative_name.replace('.', "_") +} + +pub fn find_message_descriptor<'a>( + desc_set: &'a descriptor_rust_proto::FileDescriptorSet, + message_name: &str, +) -> Option> { + // 1. Collect all message candidates whose simple name matches the target name. + let mut candidates = Vec::new(); + let simple_target = extract_simple_name(message_name); + + for file in desc_set.file() { + for message in file.message_type() { + collect_candidates(message, "", simple_target, &mut candidates); + } + } + + if candidates.is_empty() { + None + } else if candidates.len() == 1 { + // Happy path: no name collision! Highly efficient and dead simple. + Some(candidates[0].0) + } else { + // Collision path: disambiguate using suffix matching on the full Rust type path. + disambiguate_candidate(candidates, message_name) + } +} + +pub fn find_enum_descriptor<'a>( + desc_set: &'a descriptor_rust_proto::FileDescriptorSet, + enum_name: &str, +) -> Option> { + let target = format!(".{}", enum_name.strip_prefix('.').unwrap_or(enum_name)); + + for file in desc_set.file() { + let package = file.package().to_str().unwrap_or(""); + let prefix = if package.is_empty() { + ".".to_string() + } else { + format!(".{}.", package) + }; + for en in file.enum_type() { + if format!("{}{}", prefix, en.name().to_str().unwrap_or("")) == target { + return Some(en); + } + } + for msg in file.message_type() { + if let Some(found) = find_enum_nested(&prefix, msg, &target) { + return Some(found); + } + } + } + None +} + +fn find_enum_nested<'a>( + parent_prefix: &str, + msg: DescriptorProtoView<'a>, + target: &str, +) -> Option> { + let msg_name = msg.name().to_str().unwrap_or(""); + let prefix = format!("{}{}.", parent_prefix, msg_name); + + for en in msg.enum_type() { + if format!("{}{}", prefix, en.name().to_str().unwrap_or("")) == target { + return Some(en); + } + } + for nested in msg.nested_type() { + if let Some(found) = find_enum_nested(&prefix, nested, target) { + return Some(found); + } + } + None +} + +fn collect_candidates<'a>( + msg: DescriptorProtoView<'a>, + parent_path: &str, + simple_target: &str, + out: &mut Vec<(DescriptorProtoView<'a>, String)>, +) { + let name = msg.name().to_str().unwrap_or(""); + let current_path = if parent_path.is_empty() { + name.to_string() + } else { + format!("{}.{}", parent_path, name) + }; + + if name == simple_target { + out.push((msg, current_path.clone())); + } + + for nested in msg.nested_type() { + collect_candidates(nested, ¤t_path, simple_target, out); + } +} + +fn disambiguate_candidate<'a>( + candidates: Vec<(DescriptorProtoView<'a>, String)>, + target_name: &str, +) -> Option> { + let norm_target = normalize_name(target_name); + let mut best_match: Option<(DescriptorProtoView<'a>, usize)> = None; + + for (desc, path) in candidates { + let norm_path = normalize_name(&path); + if norm_target.ends_with(&norm_path) { + let match_len = norm_path.len(); + let should_update = match best_match { + Some((_, best_len)) => match_len > best_len, + None => true, + }; + if should_update { + best_match = Some((desc, match_len)); + } + } + } + + best_match.map(|(desc, _)| desc) +} + +#[cfg(test)] +mod tests { + use super::*; + use protobuf::Parse; + + fn get_bad_desc_set() -> descriptor_rust_proto::FileDescriptorSet { + const BYTES: &[u8] = include_bytes!(env!("BAD_PROTO_DESCRIPTOR_SET")); + descriptor_rust_proto::FileDescriptorSet::parse(BYTES).unwrap() + } + + fn find_msg_descriptor<'a>( + desc_set: &'a descriptor_rust_proto::FileDescriptorSet, + name: &str, + ) -> descriptor_rust_proto::DescriptorProtoView<'a> { + for file in desc_set.file() { + for message in file.message_type() { + if message.name().to_str().unwrap_or("") == name { + return message; + } + } + } + panic!("Message not found: {}", name); + } + + #[test] + fn test_unaligned_offset() { + let desc_set = get_bad_desc_set(); + let msg = find_msg_descriptor(&desc_set, "MessageUnalignedOffset"); + let err = msg.tlv_layout(&desc_set).unwrap_err(); + + // Assert un-prefixed error message + assert_eq!( + err.to_string(), + "Invalid TLV layout on field 'val' of message 'MessageUnalignedOffset': offset of 3 is not naturally aligned to its size of 4 bytes (offset must be a multiple of size)." + ); + } + + #[test] + fn test_overlapping_fields() { + let desc_set = get_bad_desc_set(); + let msg = find_msg_descriptor(&desc_set, "MessageOverlappingFields"); + let err = msg.tlv_layout(&desc_set).unwrap_err(); + + assert_eq!( + err.to_string(), + "Invalid TLV layout in message 'MessageOverlappingFields': field 'val1' (bytes 0..8) overlaps with field 'val2' (bytes 4..8) in the fixed struct." + ); + } + + #[test] + fn test_missing_offset() { + let desc_set = get_bad_desc_set(); + let msg = find_msg_descriptor(&desc_set, "MessageMissingOffset"); + let err = msg.tlv_layout(&desc_set).unwrap_err(); + + assert_eq!( + err.to_string(), + "Invalid TLV layout on field 'val' of message 'MessageMissingOffset': primitive field of type 'Type::Uint32' is missing a custom 'offset' option. All primitive fields in a TLV-serializable message must be packed in the fixed struct." + ); + } + + #[test] + fn test_missing_type_option() { + let desc_set = get_bad_desc_set(); + let msg = find_msg_descriptor(&desc_set, "MessageMissingTypeOption"); + let err = msg.tlv_layout(&desc_set).unwrap_err(); + + assert_eq!( + err.to_string(), + "Invalid TLV layout on field 'name' of message 'MessageMissingTypeOption': field of type 'Type::String' has a custom offset (0) but is missing an explicit 'type' option. Inline strings or byte arrays in the fixed struct must specify an explicit type." + ); + } + + #[test] + fn test_invalid_type_option() { + let desc_set = get_bad_desc_set(); + let msg = find_msg_descriptor(&desc_set, "MessageInvalidTypeOption"); + let err = msg.tlv_layout(&desc_set).unwrap_err(); + + assert_eq!( + err.to_string(), + "Invalid TLV layout on field 'val' of message 'MessageInvalidTypeOption': explicit type option must be a valid Rust type (got 'u24')." + ); + } + + #[test] + fn test_unsupported_field_type() { + let desc_set = get_bad_desc_set(); + let msg = find_msg_descriptor(&desc_set, "MessageUnsupportedFieldType"); + let err = msg.tlv_layout(&desc_set).unwrap_err(); + + assert_eq!( + err.to_string(), + "Invalid TLV layout on field 'name' of message 'MessageUnsupportedFieldType': Protobuf type 'Type::String' is not supported for TLV layout (only packable primitives, inline strings/bytes, and nested sub-messages are supported)." + ); + } + + #[test] + fn test_standalone_bool() { + let desc_set = get_bad_desc_set(); + let msg = find_msg_descriptor(&desc_set, "MessageStandaloneBool"); + let err = msg.tlv_layout(&desc_set).unwrap_err(); + + assert_eq!( + err.to_string(), + "Invalid TLV layout on field 'val' of message 'MessageStandaloneBool': standalone boolean fields are not supported because they cannot be safely decoded via zero-copy. Use a custom 'type' option to map the field to an integer type (e.g. `[(openprot.tlv.type) = \"u8\"]`), or place the field inside a bit-packed message (using `option (openprot.tlv.bit_packed) = ...;`)." + ); + } + + #[test] + fn test_signedness_mismatch_signed_to_unsigned() { + let desc_set = get_bad_desc_set(); + let msg = find_msg_descriptor(&desc_set, "MessageSignednessMismatchSignedToUnsigned"); + let err = msg.tlv_layout(&desc_set).unwrap_err(); + + assert_eq!( + err.to_string(), + "Invalid TLV layout on field 'val' of message 'MessageSignednessMismatchSignedToUnsigned': signedness mismatch: Protobuf field type 'Type::Sint32' (signed) does not match the signedness of the custom TLV type 'u8' (unsigned). Signed Protobuf fields must be mapped to signed Rust types (e.g. i8, i16, i32, i64)." + ); + } + + #[test] + fn test_signedness_mismatch_unsigned_to_signed() { + let desc_set = get_bad_desc_set(); + let msg = find_msg_descriptor(&desc_set, "MessageSignednessMismatchUnsignedToSigned"); + let err = msg.tlv_layout(&desc_set).unwrap_err(); + + assert_eq!( + err.to_string(), + "Invalid TLV layout on field 'val' of message 'MessageSignednessMismatchUnsignedToSigned': signedness mismatch: Protobuf field type 'Type::Uint32' (unsigned) does not match the signedness of the custom TLV type 'i8' (signed). Unsigned Protobuf fields must be mapped to unsigned Rust types (e.g. u8, u16, u32, u64)." + ); + } + + #[test] + fn test_float_to_integer_mismatch() { + let desc_set = get_bad_desc_set(); + let msg = find_msg_descriptor(&desc_set, "MessageFloatToIntegerMismatch"); + let err = msg.tlv_layout(&desc_set).unwrap_err(); + + assert_eq!( + err.to_string(), + "Invalid TLV layout on field 'val' of message 'MessageFloatToIntegerMismatch': type mismatch: Protobuf field type 'Type::Float' (floating-point) does not match the custom TLV type 'i32' (integer). Floating-point Protobuf fields must be mapped to floating-point Rust types (f32, f64)." + ); + } + + #[test] + fn test_integer_to_float_mismatch() { + let desc_set = get_bad_desc_set(); + let msg = find_msg_descriptor(&desc_set, "MessageIntegerToFloatMismatch"); + let err = msg.tlv_layout(&desc_set).unwrap_err(); + + assert_eq!( + err.to_string(), + "Invalid TLV layout on field 'val' of message 'MessageIntegerToFloatMismatch': type mismatch: Protobuf field type 'Type::Int32' (integer) does not match the custom TLV type 'f32' (floating-point). Integer Protobuf fields must be mapped to integer Rust types (e.g. i8, u8, i16, u16, i32, u32, i64, u64)." + ); + } + + #[test] + fn test_boolean_array() { + let desc_set = get_bad_desc_set(); + let msg = find_msg_descriptor(&desc_set, "MessageBooleanArray"); + let err = msg.tlv_layout(&desc_set).unwrap_err(); + + assert_eq!( + err.to_string(), + "Invalid TLV layout on field 'val' of message 'MessageBooleanArray': standalone boolean fields are not supported because they cannot be safely decoded via zero-copy. Use a custom 'type' option to map the field to an integer type (e.g. `[(openprot.tlv.type) = \"u8\"]`), or place the field inside a bit-packed message (using `option (openprot.tlv.bit_packed) = ...;`)." + ); + } + + #[test] + fn test_repeated_field_with_offset() { + let desc_set = get_bad_desc_set(); + let msg = find_msg_descriptor(&desc_set, "MessageRepeatedFieldWithOffset"); + let err = msg.tlv_layout(&desc_set).unwrap_err(); + + assert_eq!( + err.to_string(), + "Invalid TLV layout on field 'values' of message 'MessageRepeatedFieldWithOffset': repeated fields are not allowed to specify a custom 'offset' option because their size is dynamic. They must be serialized as dynamic sub-TLVs (omit the 'offset' option)." + ); + } + + #[test] + fn test_oneof_field_with_offset() { + let desc_set = get_bad_desc_set(); + let msg = find_msg_descriptor(&desc_set, "MessageOneofFieldWithOffset"); + let err = msg.tlv_layout(&desc_set).unwrap_err(); + + assert_eq!( + err.to_string(), + "Invalid TLV layout on field 'val' of message 'MessageOneofFieldWithOffset': fields inside a 'oneof' are not allowed to specify a custom 'offset' option because they are mutually exclusive. They must be serialized as dynamic sub-TLVs (omit the 'offset' option)." + ); + } + + #[test] + fn test_missing_bit_range() { + let desc_set = get_bad_desc_set(); + let msg = find_msg_descriptor(&desc_set, "MessageMissingBitRange"); + let err = msg.tlv_layout(&desc_set).unwrap_err(); + + assert_eq!( + err.to_string(), + "Invalid TLV layout on field 'val2' of message 'MessageMissingBitRange': field in a bit-packed message is missing the required 'bit_range' option." + ); + } + + #[test] + fn test_offset_or_size_in_bit_packed() { + let desc_set = get_bad_desc_set(); + let msg = find_msg_descriptor(&desc_set, "MessageOffsetOrSizeInBitPacked"); + let err = msg.tlv_layout(&desc_set).unwrap_err(); + + assert_eq!( + err.to_string(), + "Invalid TLV layout on field 'val' of message 'MessageOffsetOrSizeInBitPacked': field in a bit-packed message is not allowed to specify 'offset' or 'type' options; use the 'bit_range' option to specify its position." + ); + } + + #[test] + fn test_bit_range_on_non_packable_type() { + let desc_set = get_bad_desc_set(); + let msg = find_msg_descriptor(&desc_set, "MessageBitRangeOnNonPackableType"); + let err = msg.tlv_layout(&desc_set).unwrap_err(); + + assert_eq!( + err.to_string(), + "Invalid TLV layout on field 'name' of message 'MessageBitRangeOnNonPackableType': bit_range option is only supported on packable primitive integer, enum, or boolean types (got 'Type::String')." + ); + } + + #[test] + fn test_invalid_bit_range_format() { + let desc_set = get_bad_desc_set(); + let msg = find_msg_descriptor(&desc_set, "MessageInvalidBitRangeFormat"); + let err = msg.tlv_layout(&desc_set).unwrap_err(); + + assert_eq!( + err.to_string(), + "Invalid TLV layout on field 'val' of message 'MessageInvalidBitRangeFormat': invalid bit_range format: '7-0' (must be Verilog notation like 'high:low')." + ); + } + + #[test] + fn test_invalid_bit_range_order() { + let desc_set = get_bad_desc_set(); + let msg = find_msg_descriptor(&desc_set, "MessageInvalidBitRangeOrder"); + let err = msg.tlv_layout(&desc_set).unwrap_err(); + + assert_eq!( + err.to_string(), + "Invalid TLV layout on field 'val' of message 'MessageInvalidBitRangeOrder': invalid bit_range order '3:4': high bit must be greater than or equal to low bit." + ); + } + + #[test] + fn test_overlapping_bits() { + let desc_set = get_bad_desc_set(); + let msg = find_msg_descriptor(&desc_set, "MessageOverlappingBits"); + let err = msg.tlv_layout(&desc_set).unwrap_err(); + + assert_eq!( + err.to_string(), + "Invalid TLV layout in message 'MessageOverlappingBits': field 'val1' (bits 15:0) overlaps with field 'val2' (bits 16:15) in the bit-packed register." + ); + } + + #[test] + fn test_nested_message_not_packable() { + let desc_set = get_bad_desc_set(); + let msg = find_msg_descriptor(&desc_set, "MessageNestedMessageNotPackable"); + let err = msg.tlv_layout(&desc_set).unwrap_err(); + + assert_eq!( + err.to_string(), + "Invalid TLV layout on field 'nested' of message 'MessageNestedMessageNotPackable': nested message field has a custom offset but its type 'openprot.tlv.test.bad.NestedMsgWithTlv' is not packable inline (it must be bit-packed or a standard struct with no dynamic sub-TLVs)." + ); + } + + #[test] + fn test_overlap_fixed_sub_message() { + let desc_set = get_bad_desc_set(); + let msg = find_msg_descriptor(&desc_set, "MessageOverlapFixedSubMessage"); + let err = msg.tlv_layout(&desc_set).unwrap_err(); + + assert_eq!( + err.to_string(), + "Invalid TLV layout in message 'MessageOverlapFixedSubMessage': field 'sub' (bytes 0..16) overlaps with field 'val' (bytes 12..16) in the fixed struct." + ); + } + + #[test] + fn test_overlap_bit_packed() { + let desc_set = get_bad_desc_set(); + let msg = find_msg_descriptor(&desc_set, "MessageOverlapBitPacked"); + let err = msg.tlv_layout(&desc_set).unwrap_err(); + + assert_eq!( + err.to_string(), + "Invalid TLV layout in message 'MessageOverlapBitPacked': field 'reg' (bytes 0..4) overlaps with field 'val' (bytes 0..4) in the fixed struct." + ); + } + + #[test] + fn test_bit_packed_width_exceeded() { + let desc_set = get_bad_desc_set(); + let msg = find_msg_descriptor(&desc_set, "MessageBitPackedWidthExceeded"); + let err = msg.tlv_layout(&desc_set).unwrap_err(); + + assert_eq!( + err.to_string(), + "Invalid TLV layout on field 'val2' of message 'MessageBitPackedWidthExceeded': bit_range high bit 8 is out of bounds for the backing register width of 8 bits." + ); + } + + #[test] + fn test_invalid_bit_packed_type() { + let desc_set = get_bad_desc_set(); + let msg = find_msg_descriptor(&desc_set, "MessageInvalidBitPackedType"); + let err = msg.tlv_layout(&desc_set).unwrap_err(); + + assert_eq!( + err.to_string(), + "Invalid TLV layout in message 'MessageInvalidBitPackedType': invalid value for 'bit_packed' option: 'u64' (must be 'u8', 'u16', or 'u32')." + ); + } + + #[test] + fn test_bit_packed_as_tlv() { + let desc_set = get_bad_desc_set(); + let msg = find_msg_descriptor(&desc_set, "MessageBitPackedAsTlv"); + let err = msg.tlv_layout(&desc_set).unwrap_err(); + + assert_eq!( + err.to_string(), + "Invalid TLV layout on field 'reg' of message 'MessageBitPackedAsTlv': bit-packed message of type 'openprot.tlv.test.bad.LargePackedRegister' cannot be serialized as a dynamic sub-TLV; it must be packed inline inside the fixed struct using a custom 'offset' option." + ); + } + + #[test] + fn test_bit_packed_with_fourcc() { + let desc_set = get_bad_desc_set(); + let msg = find_msg_descriptor(&desc_set, "MessageBitPackedWithFourcc"); + let err = msg.tlv_layout(&desc_set).unwrap_err(); + + assert_eq!( + err.to_string(), + "Invalid TLV layout in message 'MessageBitPackedWithFourcc': message is bit-packed and cannot specify a 'tlv_fourcc' option; bit-packed messages are raw registers and cannot be serialized as TLV blocks." + ); + } + + #[test] + fn test_unaligned_bit_packed() { + let desc_set = get_bad_desc_set(); + let msg = find_msg_descriptor(&desc_set, "MessageUnalignedBitPacked"); + let err = msg.tlv_layout(&desc_set).unwrap_err(); + + assert_eq!( + err.to_string(), + "Invalid TLV layout on field 'reg' of message 'MessageUnalignedBitPacked': offset of 2 is not naturally aligned to its size of 4 bytes (offset must be a multiple of size)." + ); + } + + #[test] + fn test_unaligned_nested_struct() { + let desc_set = get_bad_desc_set(); + let msg = find_msg_descriptor(&desc_set, "MessageUnalignedNestedStruct"); + let err = msg.tlv_layout(&desc_set).unwrap_err(); + + assert_eq!( + err.to_string(), + "Invalid TLV layout on field 'sub' of message 'MessageUnalignedNestedStruct': offset of 2 is not naturally aligned to its size of 4 bytes (offset must be a multiple of size)." + ); + } + + #[test] + fn test_zero_size_inline_string() { + let desc_set = get_bad_desc_set(); + let msg = find_msg_descriptor(&desc_set, "MessageZeroSizeInlineString"); + let err = msg.tlv_layout(&desc_set).unwrap_err(); + + assert_eq!( + err.to_string(), + "Invalid TLV layout on field 'name' of message 'MessageZeroSizeInlineString': explicit size option for inline string or bytes fields must be greater than 0." + ); + } + + #[test] + fn test_nested_message_in_bit_packed() { + let desc_set = get_bad_desc_set(); + let msg = find_msg_descriptor(&desc_set, "MessageNestedMessageInBitPacked"); + let err = msg.tlv_layout(&desc_set).unwrap_err(); + + assert_eq!( + err.to_string(), + "Invalid TLV layout on field 'nested' of message 'MessageNestedMessageInBitPacked': bit_range option is only supported on packable primitive integer, enum, or boolean types (got 'Type::Message')." + ); + } + + #[test] + fn test_float_in_bit_packed() { + let desc_set = get_bad_desc_set(); + let msg = find_msg_descriptor(&desc_set, "MessageFloatInBitPacked"); + let err = msg.tlv_layout(&desc_set).unwrap_err(); + + assert_eq!( + err.to_string(), + "Invalid TLV layout on field 'val' of message 'MessageFloatInBitPacked': bit_range option is only supported on packable primitive integer, enum, or boolean types (got 'Type::Float')." + ); + } + + #[test] + fn test_type_in_bit_packed() { + let desc_set = get_bad_desc_set(); + let msg = find_msg_descriptor(&desc_set, "MessageTypeInBitPacked"); + let err = msg.tlv_layout(&desc_set).unwrap_err(); + + assert_eq!( + err.to_string(), + "Invalid TLV layout on field 'val' of message 'MessageTypeInBitPacked': field in a bit-packed message is not allowed to specify 'offset' or 'type' options; use the 'bit_range' option to specify its position." + ); + } + #[test] + fn test_name_collision_resolution() { + // Reconstruct FileDescriptorSet from the edge_cases DescriptorInfo + let info = &edge_cases_rust_proto::__unstable::SHARED_TLV_TEST_EDGE_CASES_DESCRIPTOR_INFO; + + let desc_set = collect_descriptor_set(info).expect("Failed to collect descriptor set"); + + // 1. Resolve using the fully-qualified name of the nested message + let nested_desc = find_message_descriptor( + &desc_set, + "openprot.tlv.test.edge_cases.MessageCollisionContainer.PackedRegister", + ) + .expect("Failed to find nested MessageCollisionContainer.PackedRegister"); + + // The nested message has only a single field: 'val' + let mut nested_field_names = Vec::new(); + for field in nested_desc.field() { + nested_field_names.push(field.name().to_str().unwrap().to_string()); + } + assert_eq!(nested_field_names, vec!["val"]); + + // 2. Resolve using the fully-qualified name of the top-level message + let top_desc = + find_message_descriptor(&desc_set, "openprot.tlv.test.edge_cases.PackedRegister") + .expect("Failed to find top-level PackedRegister"); + + // The top-level message has fields: 'enabled', 'mode', 'status', 'val' + let mut top_field_names = Vec::new(); + for field in top_desc.field() { + top_field_names.push(field.name().to_str().unwrap().to_string()); + } + assert_eq!(top_field_names, vec!["enabled", "mode", "status", "val"]); + } + + #[test] + fn test_unaligned_inline_string() { + let desc_set = get_bad_desc_set(); + let msg = find_msg_descriptor(&desc_set, "MessageUnalignedInlineString"); + let err = msg.tlv_layout(&desc_set).unwrap_err(); + + assert_eq!( + err.to_string(), + "Invalid TLV layout on field 'name' of message 'MessageUnalignedInlineString': offset of 3 is not naturally aligned to its size of 4 bytes (offset must be a multiple of size)." + ); + } + + #[test] + fn test_unaligned_inline_bytes() { + let desc_set = get_bad_desc_set(); + let msg = find_msg_descriptor(&desc_set, "MessageUnalignedInlineBytes"); + let err = msg.tlv_layout(&desc_set).unwrap_err(); + + assert_eq!( + err.to_string(), + "Invalid TLV layout on field 'data' of message 'MessageUnalignedInlineBytes': offset of 3 is not naturally aligned to its size of 4 bytes (offset must be a multiple of size)." + ); + } + + #[test] + fn test_error_string_nested_message_not_found() { + let err = TlvLayoutError { + message_name: "MyMessage".to_string(), + field_name: Some("my_field".to_string()), + kind: TlvLayoutErrorKind::NestedMessageNotFound { + type_name: "NonExistentMessage".to_string(), + }, + }; + assert_eq!( + err.to_string(), + "Invalid TLV layout on field 'my_field' of message 'MyMessage': nested message type descriptor for 'NonExistentMessage' was not found in the descriptor set." + ); + } + + #[test] + fn test_missing_tlv_fourcc_sub() { + let desc_set = get_bad_desc_set(); + let msg = find_msg_descriptor(&desc_set, "MessageWithMissingFourccSub"); + let err = msg.tlv_layout(&desc_set).unwrap_err(); + + assert_eq!( + err.to_string(), + "Invalid TLV layout on field 'sub' of message 'MessageWithMissingFourccSub': nested message 'openprot.tlv.test.bad.MissingFourccSub' acts as a dynamic sub-TLV but is missing a 'tlv_fourcc' option; sub-TLVs must have a unique 4-character tag." + ); + } + + #[test] + fn test_duplicate_tlv_tag() { + let desc_set = get_bad_desc_set(); + let msg = find_msg_descriptor(&desc_set, "MessageWithDuplicateTlvTags"); + let err = msg.tlv_layout(&desc_set).unwrap_err(); + + assert_eq!( + err.to_string(), + "Invalid TLV layout in message 'MessageWithDuplicateTlvTags': sub-TLV fields 'gpu' and 'npu' share the same tlv_fourcc tag 'ACCL'; all sub-TLV fields within a message must have unique tags to prevent decoding ambiguity." + ); + } + + #[test] + fn test_circular_dependency() { + let desc_set = get_bad_desc_set(); + let msg = find_msg_descriptor(&desc_set, "CircularA"); + let err = msg + .tlv_layout_with_name("openprot.tlv.test.bad.CircularA", &desc_set) + .unwrap_err(); + + assert_eq!( + err.to_string(), + "Invalid TLV layout on field 'b' of message 'CircularA': circular dependency detected: message 'openprot.tlv.test.bad.CircularA' directly or indirectly contains itself inline, which requires infinite struct size." + ); + } + + #[test] + fn test_multiple_extra_data_fields() { + let desc_set = get_bad_desc_set(); + let msg = find_msg_descriptor(&desc_set, "MessageMultipleExtraDataFields"); + let err = msg.tlv_layout(&desc_set).unwrap_err(); + + assert_eq!( + err.to_string(), + "Invalid TLV layout in message 'MessageMultipleExtraDataFields': message cannot have more than one field with 'extra_data' option." + ); + } + + #[test] + fn test_extra_data_with_sub_tlvs() { + let desc_set = get_bad_desc_set(); + let msg = find_msg_descriptor(&desc_set, "MessageExtraDataWithSubTlvs"); + let err = msg.tlv_layout(&desc_set).unwrap_err(); + + assert_eq!( + err.to_string(), + "Invalid TLV layout in message 'MessageExtraDataWithSubTlvs': message with an 'extra_data' field cannot also contain dynamic sub-TLVs." + ); + } + + #[test] + fn test_extra_data_on_invalid_type() { + let desc_set = get_bad_desc_set(); + let msg = find_msg_descriptor(&desc_set, "MessageExtraDataOnInvalidType"); + let err = msg.tlv_layout(&desc_set).unwrap_err(); + + assert_eq!( + err.to_string(), + "Invalid TLV layout on field 'val' of message 'MessageExtraDataOnInvalidType': 'extra_data' option is only supported on 'string' or 'bytes' fields (got 'Type::Uint32')." + ); + } + + #[test] + fn test_extra_data_with_offset() { + let desc_set = get_bad_desc_set(); + let msg = find_msg_descriptor(&desc_set, "MessageExtraDataWithOffset"); + let err = msg.tlv_layout(&desc_set).unwrap_err(); + + assert_eq!( + err.to_string(), + "Invalid TLV layout on field 'ext' of message 'MessageExtraDataWithOffset': field with 'extra_data' option cannot specify a custom 'offset' option." + ); + } + + #[test] + fn test_repeated_extra_data() { + let desc_set = get_bad_desc_set(); + let msg = find_msg_descriptor(&desc_set, "MessageRepeatedExtraData"); + let err = msg.tlv_layout(&desc_set).unwrap_err(); + + assert_eq!( + err.to_string(), + "Invalid TLV layout on field 'chunks' of message 'MessageRepeatedExtraData': field with 'extra_data' option cannot be 'repeated'." + ); + } + + #[test] + fn test_oneof_extra_data() { + let desc_set = get_bad_desc_set(); + let msg = find_msg_descriptor(&desc_set, "MessageOneofExtraData"); + let err = msg.tlv_layout(&desc_set).unwrap_err(); + + assert_eq!( + err.to_string(), + "Invalid TLV layout on field 'ext' of message 'MessageOneofExtraData': field with 'extra_data' option cannot be inside a 'oneof'." + ); + } + + #[test] + fn test_extra_data_missing_type_option() { + let desc_set = get_bad_desc_set(); + let msg = find_msg_descriptor(&desc_set, "MessageExtraDataMissingType"); + let err = msg.tlv_layout(&desc_set).unwrap_err(); + + assert_eq!( + err.to_string(), + "Invalid TLV layout on field 'ext' of message 'MessageExtraDataMissingType': field with 'extra_data' option must explicitly specify a 'type' option (e.g., `[(openprot.tlv.type) = \"[u8]\"]`)." + ); + } + + #[test] + fn test_extra_data_unsupported_type() { + let desc_set = get_bad_desc_set(); + let msg = find_msg_descriptor(&desc_set, "MessageExtraDataUnsupportedType"); + let err = msg.tlv_layout(&desc_set).unwrap_err(); + + assert_eq!( + err.to_string(), + "Invalid TLV layout on field 'ext' of message 'MessageExtraDataUnsupportedType': 'extra_data' option currently only supports type '[u8]' (got '[u32]')." + ); + } +} diff --git a/shared/tlv/rules.bzl b/shared/tlv/rules.bzl new file mode 100644 index 00000000..1ad3733f --- /dev/null +++ b/shared/tlv/rules.bzl @@ -0,0 +1,227 @@ +# Copyright 2026 Google LLC +# +# 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. + +"""Bazel rules for Protobuf-to-TLV code generation.""" + +load("@protobuf//bazel/common:proto_info.bzl", "ProtoInfo") +load("@rules_rust//rust:defs.bzl", "rust_library") + +def _tlv_codegen_rule_impl(ctx): + # Declare the output Rust file + output_file = ctx.actions.declare_file(ctx.label.name + ".rs") + + # Collect all transitive descriptor set files from the proto dependency + proto_info = ctx.attr.proto[ProtoInfo] + descriptor_sets = proto_info.transitive_descriptor_sets.to_list() + + # Get the rustfmt toolchain + rustfmt_toolchain = ctx.toolchains["@rules_rust//rust/rustfmt:toolchain_type"] + + # Build the arguments for the codegen tool + args = ctx.actions.args() + for ds in descriptor_sets: + args.add("--descriptor-set", ds.path) + if proto_info.direct_descriptor_set: + args.add("--direct-descriptor-set", proto_info.direct_descriptor_set.path) + args.add_all(proto_info.direct_sources, before_each = "--direct-proto") + for dep in ctx.attr.deps: + args.add("--import-crate", dep.label.name.replace("-", "_")) + args.add("--output", output_file.path) + if ctx.attr.test_label: + args.add("--test-label", ctx.attr.test_label) + + # Run the codegen tool + ctx.actions.run( + outputs = [output_file], + inputs = depset( + direct = descriptor_sets, + transitive = [rustfmt_toolchain.all_files], + ), + executable = ctx.executable._codegen_tool, + arguments = [args], + env = { + "RUSTFMT": rustfmt_toolchain.rustfmt.path, + }, + mnemonic = "TlvCodegen", + progress_message = "Generating and formatting TLV Rust structs for %{label}", + ) + + # Return the generated file + return [ + DefaultInfo(files = depset([output_file])), + ] + +_tlv_codegen_rule = rule( + implementation = _tlv_codegen_rule_impl, + attrs = { + "proto": attr.label( + mandatory = True, + providers = [ProtoInfo], + doc = "The proto_library target to generate TLV structs for.", + ), + "deps": attr.label_list( + default = [], + doc = "Upstream TLV libraries this generated crate depends on.", + ), + "_codegen_tool": attr.label( + default = Label("//shared/tlv:codegen"), + executable = True, + cfg = "exec", + ), + "test_label": attr.string( + doc = "The label of the diff test target if a checked-in source is used.", + ), + }, + toolchains = ["@rules_rust//rust/rustfmt:toolchain_type"], + doc = "Generates zero-copy Rust TLV structs from a proto_library.", +) + +def _update_golden_impl(ctx): + file_a = ctx.file.file_a + file_b = ctx.file.file_b + script = ctx.actions.declare_file(ctx.label.name + "_update.sh") + + content = """#!/bin/bash +FILE_A="{file_a_path}" +FILE_B="{file_b_path}" + +if [ -z "$BUILD_WORKSPACE_DIRECTORY" ]; then + echo "Error: This target must be run using 'bazel run'." >&2 + exit 1 +fi + +DEST="$BUILD_WORKSPACE_DIRECTORY/$FILE_A" +echo "Updating checked-in file: $DEST" +cp "$FILE_B" "$DEST" +chmod +w "$DEST" +""".format( + file_a_path = file_a.short_path, + file_b_path = file_b.short_path, + ) + + ctx.actions.write( + output = script, + content = content, + is_executable = True, + ) + + return [ + DefaultInfo( + executable = script, + runfiles = ctx.runfiles(files = [file_a, file_b]), + ), + ] + +_update_golden = rule( + implementation = _update_golden_impl, + attrs = { + "file_a": attr.label(allow_single_file = True, mandatory = True), + "file_b": attr.label(allow_single_file = True, mandatory = True), + }, + executable = True, + doc = "Copies a generated file back to the source tree when executed via 'bazel run'.", +) + +def tlv_codegen(name, proto, deps = [], src = None, visibility = None, **kwargs): + """Generates a first-class Rust crate containing TLV structs from a proto_library. + + If `src` is specified, it compiles the checked-in file directly and defines a companion + update binary target named `_update` to ensure the checked-in file can be updated. + + Args: + name: The name of the generated rust_library target. + proto: The proto_library target to generate structs for. + deps: Optional list of upstream TLV libraries this crate depends on. + src: Optional checked-in Rust source file. + visibility: The visibility of the target. + **kwargs: Additional arguments passed to the underlying rust_library. + """ + gen_file_target = name + "_gen_file" + + # 1. Define the action to generate the .rs source file + _tlv_codegen_rule( + name = gen_file_target, + proto = proto, + deps = deps, + test_label = "//{}:{}_update".format(native.package_name(), name) if src else None, + visibility = ["//visibility:private"], + ) + + # 2. Compile the rust_library (using either checked-in src or generated file) + rust_src = src if src else ":" + gen_file_target + + compile_data = kwargs.pop("compile_data", []) + if src: + verified_src_target = name + "_verified_src" + native.genrule( + name = verified_src_target, + srcs = [src, ":" + gen_file_target], + outs = [name + "_verified.txt"], + cmd = """ + if ! diff -u $(location {src}) $(location :{gen}); then + echo "" >&2 + echo "================================================================================" >&2 + echo "Error: Checked-in file '$(location {src})' does not match the generated contents." >&2 + echo "To automatically update the checked-in file, run:" >&2 + echo " bazel run //{package}:{name}_update" >&2 + echo "================================================================================" >&2 + exit 1 + fi + touch $@ + """.format( + src = src, + gen = gen_file_target, + package = native.package_name(), + name = name, + ), + visibility = ["//visibility:private"], + ) + + # Force dependency on the verification genrule at build-time, preserving perfect IDE support on `src` + compile_data = compile_data + [":" + verified_src_target] + + tags = kwargs.get("tags", []) + for t in ["no-format", "no-rustfmt", "norustfmt", "no-clippy"]: + if t not in tags: + tags.append(t) + kwargs["tags"] = tags + + rust_library( + name = name, + srcs = [rust_src], + crate_name = name, + edition = "2024", + visibility = visibility, + compile_data = compile_data, + deps = [ + Label("//shared/aligned"), + Label("//shared/tlv"), + Label("//third_party:bstr"), + Label("//third_party:zerocopy"), + ] + deps, + proc_macro_deps = [ + Label("//third_party:bitfield_struct"), + ], + **kwargs + ) + + # 3. If checked-in src is used, define a companion update target + if src: + _update_golden( + name = name + "_update", + file_a = src, + file_b = ":" + gen_file_target, + tags = ["tlv_update"], + visibility = visibility, + ) diff --git a/shared/tlv/test/BUILD.bazel b/shared/tlv/test/BUILD.bazel new file mode 100644 index 00000000..12719b9b --- /dev/null +++ b/shared/tlv/test/BUILD.bazel @@ -0,0 +1,157 @@ +# Copyright 2026 Google LLC +# +# 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. + +load("@protobuf//bazel:proto_library.bzl", "proto_library") +load("@protobuf//rust:defs.bzl", "rust_proto_library") +load("//shared/tlv:rules.bzl", "tlv_codegen") + +proto_library( + name = "soc_proto", + srcs = ["soc.proto"], + visibility = ["//visibility:public"], + deps = [ + "//shared/tlv:options_proto", + ], +) + +rust_proto_library( + name = "soc_rust_proto", + tags = ["manual"], + visibility = ["//visibility:public"], + deps = [":soc_proto"], +) + +proto_library( + name = "edge_cases_proto", + srcs = ["edge_cases.proto"], + visibility = ["//visibility:public"], + deps = [ + "//shared/tlv:options_proto", + ], +) + +rust_proto_library( + name = "edge_cases_rust_proto", + tags = ["manual"], + visibility = ["//visibility:public"], + deps = [":edge_cases_proto"], +) + +proto_library( + name = "bad_proto", + srcs = ["bad.proto"], + visibility = ["//visibility:public"], + deps = [ + "//shared/tlv:options_proto", + ], +) + +rust_proto_library( + name = "bad_rust_proto", + tags = ["manual"], + visibility = ["//visibility:public"], + deps = [":bad_proto"], +) + +tlv_codegen( + name = "soc_tlv", + src = "soc_tlv.rs", + proto = ":soc_proto", + visibility = ["//visibility:public"], +) + +tlv_codegen( + name = "edge_cases_tlv", + src = "edge_cases_tlv.rs", + proto = ":edge_cases_proto", + visibility = ["//visibility:public"], +) + +proto_library( + name = "import_base_proto", + srcs = ["import_base.proto"], + visibility = ["//visibility:public"], + deps = [ + "//shared/tlv:options_proto", + ], +) + +tlv_codegen( + name = "import_base_tlv", + src = "import_base_tlv.rs", + proto = ":import_base_proto", + visibility = ["//visibility:public"], +) + +proto_library( + name = "import_simple_proto", + srcs = ["import_simple.proto"], + visibility = ["//visibility:public"], + deps = [ + ":import_base_proto", + "//shared/tlv:options_proto", + ], +) + +tlv_codegen( + name = "import_simple_tlv", + src = "import_simple_tlv.rs", + proto = ":import_simple_proto", + visibility = ["//visibility:public"], + deps = [":import_base_tlv"], +) + +proto_library( + name = "import_layered_proto", + srcs = ["import_layered.proto"], + visibility = ["//visibility:public"], + deps = [ + ":import_base_proto", + ":import_simple_proto", + "//shared/tlv:options_proto", + ], +) + +tlv_codegen( + name = "import_layered_tlv", + src = "import_layered_tlv.rs", + proto = ":import_layered_proto", + visibility = ["//visibility:public"], + deps = [ + ":import_base_tlv", + ":import_simple_tlv", + ], +) + +proto_library( + name = "import_pure_proto", + visibility = ["//visibility:public"], + deps = [ + ":import_base_proto", + ":import_layered_proto", + ":import_simple_proto", + ], +) + +tlv_codegen( + name = "import_pure_tlv", + src = "import_pure_tlv.rs", + proto = ":import_pure_proto", + visibility = ["//visibility:public"], + deps = [ + ":import_base_tlv", + ":import_layered_tlv", + ":import_simple_tlv", + ], +) diff --git a/shared/tlv/test/bad.proto b/shared/tlv/test/bad.proto new file mode 100644 index 00000000..3caa9cd0 --- /dev/null +++ b/shared/tlv/test/bad.proto @@ -0,0 +1,385 @@ +// Copyright 2026 Google LLC +// +// 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. + +syntax = "proto3"; + +package openprot.tlv.test.bad; + +import "shared/tlv/options.proto"; + +message NestedMsg { + option (openprot.tlv.tlv_fourcc) = "NEST"; + uint32 dummy = 1 [(openprot.tlv.offset) = 0]; +} + +// 1. Message with an unaligned offset for a 4-byte integer (offset 3 is not +// aligned to 4) +message MessageUnalignedOffset { + option (openprot.tlv.tlv_fourcc) = "UNAL"; + uint32 val = 1 [(openprot.tlv.offset) = 3]; +} + +// 2. Message with overlapping fields in the fixed struct (both naturally +// aligned, but overlapping) +message MessageOverlappingFields { + option (openprot.tlv.tlv_fourcc) = "OVER"; + uint64 val1 = 1 [(openprot.tlv.offset) = 0]; // Bytes 0-7 (aligned at 0) + uint32 val2 = 2 + [(openprot.tlv.offset) = 4]; // Bytes 4-7 (aligned at 4, overlaps!) +} + +// 4. Message with a primitive field missing the offset option +message MessageMissingOffset { + option (openprot.tlv.tlv_fourcc) = "MOFF"; + uint32 val = 1; // Primitive integer missing offset +} + +// 5. Message with a string field having an offset but missing explicit type +message MessageMissingTypeOption { + option (openprot.tlv.tlv_fourcc) = "MSIZ"; + string name = 1 [(openprot.tlv.offset) = 0]; // Missing type +} + +// 6. Message with an invalid type override (u24 is invalid in Rust) +message MessageInvalidTypeOption { + option (openprot.tlv.tlv_fourcc) = "ITYP"; + uint32 val = 1 [(openprot.tlv.offset) = 0, (openprot.tlv.type) = "u24"]; +} + +// 7. Message with an unsupported field type (e.g. string without offset) +message MessageUnsupportedFieldType { + option (openprot.tlv.tlv_fourcc) = "UNSP"; + string name = 1; // String without offset +} + +// 8. Message with a missing bit_range option on a field in a bit-packed message +message MessageMissingBitRange { + option (openprot.tlv.bit_packed) = "u32"; + uint32 val1 = 1 [(openprot.tlv.bit_range) = "4:0"]; + uint32 val2 = 2; // Missing bit_range! +} + +// 9. Message with offset or size options in a bit-packed message +message MessageOffsetOrSizeInBitPacked { + option (openprot.tlv.bit_packed) = "u32"; + uint32 val = 1 [ + (openprot.tlv.bit_range) = "4:0", + (openprot.tlv.offset) = 0 // Not allowed in bit-packed! + ]; +} + +// 9b. Message with a type option in a bit-packed message +message MessageTypeInBitPacked { + option (openprot.tlv.bit_packed) = "u32"; + uint32 val = 1 [ + (openprot.tlv.bit_range) = "4:0", + (openprot.tlv.type) = "u8" // Not allowed in bit-packed! + ]; +} + +// 10. Message with bit_range on a non-packable type (string) in a bit-packed +// message +message MessageBitRangeOnNonPackableType { + option (openprot.tlv.bit_packed) = "u32"; + string name = 1 [(openprot.tlv.bit_range) = "7:0"]; // Non-packable! +} + +// 11. Message with an invalid bit_range format +message MessageInvalidBitRangeFormat { + option (openprot.tlv.bit_packed) = "u32"; + uint32 val = 1 [(openprot.tlv.bit_range) = "7-0"]; // Invalid format! +} + +// 11b. Message with an invalid bit_range order (high < low) +message MessageInvalidBitRangeOrder { + option (openprot.tlv.bit_packed) = "u32"; + uint32 val = 1 [(openprot.tlv.bit_range) = "3:4"]; // Invalid order! +} + +// 11c. Message with bit-level overlapping fields inside the register +message MessageOverlappingBits { + option (openprot.tlv.bit_packed) = "u32"; + uint32 val1 = 1 [(openprot.tlv.bit_range) = "15:0"]; + uint32 val2 = 2 [(openprot.tlv.bit_range) = "16:15"]; // Overlaps on bit 15! +} + +message NestedMsgWithTlv { + option (openprot.tlv.tlv_fourcc) = "DYN_"; + NestedMsg sub = 1; // Dynamic sub-TLV! +} + +// 12. Message with a nested message that has an offset but is not packable (it +// has a dynamic sub-TLV) +message MessageNestedMessageNotPackable { + option (openprot.tlv.tlv_fourcc) = "NNPK"; + NestedMsgWithTlv nested = 1 [(openprot.tlv.offset) = 0]; +} + +message LargeNestedMsg { + uint32 val1 = 1 [(openprot.tlv.offset) = 0]; + uint32 val2 = 2 [(openprot.tlv.offset) = 4]; + uint32 val3 = 3 [(openprot.tlv.offset) = 8]; + uint32 val4 = 4 [(openprot.tlv.offset) = 12]; +} + +// 13. Message with a field overlapping an inline nested standard struct (fixed +// sub-message) +message MessageOverlapFixedSubMessage { + option (openprot.tlv.tlv_fourcc) = "OLFS"; + LargeNestedMsg sub = 1 [(openprot.tlv.offset) = 0]; // Range 0..16 + uint32 val = 2 [(openprot.tlv.offset) = 12]; // Range 12..16, overlaps sub! +} + +message LargePackedRegister { + option (openprot.tlv.bit_packed) = "u32"; + uint32 val1 = 1 [(openprot.tlv.bit_range) = "15:0"]; + uint32 val2 = 2 [(openprot.tlv.bit_range) = "31:16"]; +} + +// 14. Message with a field overlapping an inline nested bit-packed message +message MessageOverlapBitPacked { + option (openprot.tlv.tlv_fourcc) = "OLBP"; + LargePackedRegister reg = 1 [(openprot.tlv.offset) = 0]; // Range 0..4 + uint32 val = 2 [(openprot.tlv.offset) = 0]; // Range 0..4, overlaps reg! +} + +// 15. Message with bit_range high bit out of bounds for backing width (8 is out +// of bounds for u8/8-bit!) +message MessageBitPackedWidthExceeded { + option (openprot.tlv.bit_packed) = "u8"; + uint32 val1 = 1 [(openprot.tlv.bit_range) = "4:0"]; + uint32 val2 = 2 + [(openprot.tlv.bit_range) = "8:5"]; // High bit 8 is out of bounds! +} + +// 16. Message with an invalid bit_packed backing type string +message MessageInvalidBitPackedType { + option (openprot.tlv.bit_packed) = "u64"; // Invalid backing type! + uint32 val = 1 [(openprot.tlv.bit_range) = "7:0"]; +} + +// 17. Message with a bit-packed message nested without offset (invalid, cannot +// be tlv!) +message MessageBitPackedAsTlv { + option (openprot.tlv.tlv_fourcc) = "BPTL"; + LargePackedRegister reg = 1; // Missing offset! +} + +// 18. Message that is bit-packed and specifies a tlv_fourcc option (invalid!) +message MessageBitPackedWithFourcc { + option (openprot.tlv.bit_packed) = "u32"; + option (openprot.tlv.tlv_fourcc) = "BPFC"; + uint32 val = 1 [(openprot.tlv.bit_range) = "7:0"]; +} + +// 19. Message with an unaligned nested bit-packed message (size 4, offset 2) +message MessageUnalignedBitPacked { + option (openprot.tlv.tlv_fourcc) = "UNBP"; + LargePackedRegister reg = 1 [(openprot.tlv.offset) = 2]; +} + +// 20. Message with an unaligned nested standard struct (max_align 4, offset 2) +message MessageUnalignedNestedStruct { + option (openprot.tlv.tlv_fourcc) = "UNNS"; + LargeNestedMsg sub = 1 [(openprot.tlv.offset) = 2]; +} + +// 21. Message with an inline string of size 0 +message MessageZeroSizeInlineString { + option (openprot.tlv.tlv_fourcc) = "ZSTR"; + string name = 1 [(openprot.tlv.offset) = 0, (openprot.tlv.type) = "[u8; 0]"]; +} + +// 22. Message with a nested message inside a bit-packed message (invalid!) +message MessageNestedMessageInBitPacked { + option (openprot.tlv.bit_packed) = "u32"; + NestedMsg nested = 1 [(openprot.tlv.bit_range) = "7:0"]; +} + +// 23. Message with a float inside a bit-packed message (invalid!) +message MessageFloatInBitPacked { + option (openprot.tlv.bit_packed) = "u32"; + float val = 1 [(openprot.tlv.bit_range) = "7:0"]; +} + +// 24. Message with an unaligned inline string (size 16, type [u32; 4], offset 3 +// is not aligned to 4) +message MessageUnalignedInlineString { + option (openprot.tlv.tlv_fourcc) = "UNST"; + string name = 1 [(openprot.tlv.offset) = 3, (openprot.tlv.type) = "[u32; 4]"]; +} + +// 25. Message with an unaligned inline bytes (size 16, type [u32; 4], offset 3 +// is not aligned to 4) +message MessageUnalignedInlineBytes { + option (openprot.tlv.tlv_fourcc) = "UNBY"; + bytes data = 1 [(openprot.tlv.offset) = 3, (openprot.tlv.type) = "[u32; 4]"]; +} + +// 26. Message with a standalone boolean field (not allowed, cannot derive +// FromBytes) +message MessageStandaloneBool { + option (openprot.tlv.tlv_fourcc) = "BOOL"; + bool val = 1 [(openprot.tlv.offset) = 0]; +} + +// 27. Message with signedness mismatch: signed proto type mapped to unsigned +// TLV type +message MessageSignednessMismatchSignedToUnsigned { + option (openprot.tlv.tlv_fourcc) = "MSM1"; + sint32 val = 1 [(openprot.tlv.offset) = 0, (openprot.tlv.type) = "u8"]; +} + +// 28. Message with signedness mismatch: unsigned proto type mapped to signed +// TLV type +message MessageSignednessMismatchUnsignedToSigned { + option (openprot.tlv.tlv_fourcc) = "MSM2"; + uint32 val = 1 [(openprot.tlv.offset) = 0, (openprot.tlv.type) = "i8"]; +} + +// 29. Message with float-to-integer mismatch +message MessageFloatToIntegerMismatch { + option (openprot.tlv.tlv_fourcc) = "FIM1"; + float val = 1 [(openprot.tlv.offset) = 0, (openprot.tlv.type) = "i32"]; +} + +// 30. Message with integer-to-float mismatch +message MessageIntegerToFloatMismatch { + option (openprot.tlv.tlv_fourcc) = "FIM2"; + int32 val = 1 [(openprot.tlv.offset) = 0, (openprot.tlv.type) = "f32"]; +} + +// 31. Message with a boolean array (not allowed) +message MessageBooleanArray { + option (openprot.tlv.tlv_fourcc) = "BAR1"; + bytes val = 1 [(openprot.tlv.offset) = 0, (openprot.tlv.type) = "[bool; 4]"]; +} + +// 32. Message with a repeated field that has a custom offset option (not +// allowed) +message MessageRepeatedFieldWithOffset { + option (openprot.tlv.tlv_fourcc) = "REPO"; + repeated uint32 values = 1 [(openprot.tlv.offset) = 0]; +} + +// 33. Message with a oneof field that has a custom offset option (not allowed) +message MessageOneofFieldWithOffset { + option (openprot.tlv.tlv_fourcc) = "ONEO"; + oneof my_oneof { + uint32 val = 1 [(openprot.tlv.offset) = 0]; + } +} +// 34. Message with a nested message acting as a dynamic sub-TLV, but the nested +// message lacks a tlv_fourcc option +message MissingFourccSub { + uint32 val = 1 [(openprot.tlv.offset) = 0]; +} + +message MessageWithMissingFourccSub { + option (openprot.tlv.tlv_fourcc) = "MFOU"; + MissingFourccSub sub = 1; +} + +// 35. Message with duplicate sub-TLV tags (FakeGpu and FakeNpu share the same +// tag ACCL) +message FakeGpu { + option (openprot.tlv.tlv_fourcc) = "ACCL"; + uint32 cores = 1 [(openprot.tlv.offset) = 0]; +} + +message FakeNpu { + option (openprot.tlv.tlv_fourcc) = "ACCL"; + uint32 tops = 1 [(openprot.tlv.offset) = 0]; +} + +message MessageWithDuplicateTlvTags { + option (openprot.tlv.tlv_fourcc) = "DUPL"; + FakeGpu gpu = 1; + FakeNpu npu = 2; +} + +// 36. Circular inline nested messages (CircularA contains CircularB inline, +// CircularB contains CircularA inline) +message CircularA { + option (openprot.tlv.tlv_fourcc) = "CIRA"; + CircularB b = 1 [(openprot.tlv.offset) = 0]; +} + +message CircularB { + option (openprot.tlv.tlv_fourcc) = "CIRB"; + CircularA a = 1 [(openprot.tlv.offset) = 0]; +} + +// 37. Message with multiple extra_data fields +message MessageMultipleExtraDataFields { + option (openprot.tlv.tlv_fourcc) = "MEDF"; + bytes ext1 = 1 + [(openprot.tlv.extra_data) = true, (openprot.tlv.type) = "[u8]"]; + bytes ext2 = 2 + [(openprot.tlv.extra_data) = true, (openprot.tlv.type) = "[u8]"]; +} + +// 38. Message with extra_data field AND dynamic sub-TLVs +message MessageExtraDataWithSubTlvs { + option (openprot.tlv.tlv_fourcc) = "EDST"; + bytes ext = 1 + [(openprot.tlv.extra_data) = true, (openprot.tlv.type) = "[u8]"]; + NestedMsg sub = 2; +} + +// 39. Message with extra_data on a non-string/bytes type (uint32) +message MessageExtraDataOnInvalidType { + option (openprot.tlv.tlv_fourcc) = "EDIT"; + uint32 val = 1 + [(openprot.tlv.extra_data) = true, (openprot.tlv.type) = "[u8]"]; +} + +// 40. Message with extra_data AND offset option +message MessageExtraDataWithOffset { + option (openprot.tlv.tlv_fourcc) = "EDWO"; + bytes ext = 1 [ + (openprot.tlv.offset) = 0, + (openprot.tlv.extra_data) = true, + (openprot.tlv.type) = "[u8]" + ]; +} + +// 41. Message with repeated field marked as extra_data +message MessageRepeatedExtraData { + option (openprot.tlv.tlv_fourcc) = "MRED"; + repeated bytes chunks = 1 + [(openprot.tlv.extra_data) = true, (openprot.tlv.type) = "[u8]"]; +} + +// 42. Message with extra_data field inside a oneof +message MessageOneofExtraData { + option (openprot.tlv.tlv_fourcc) = "MOED"; + oneof payload { + bytes ext = 1 + [(openprot.tlv.extra_data) = true, (openprot.tlv.type) = "[u8]"]; + } +} + +// 43. Message with extra_data field missing type option +message MessageExtraDataMissingType { + option (openprot.tlv.tlv_fourcc) = "EDMT"; + bytes ext = 1 [(openprot.tlv.extra_data) = true]; +} + +// 44. Message with extra_data field having unsupported type option (not [u8]) +message MessageExtraDataUnsupportedType { + option (openprot.tlv.tlv_fourcc) = "EDUT"; + bytes ext = 1 + [(openprot.tlv.extra_data) = true, (openprot.tlv.type) = "[u32]"]; +} diff --git a/shared/tlv/test/edge_cases.proto b/shared/tlv/test/edge_cases.proto new file mode 100644 index 00000000..cca5ac92 --- /dev/null +++ b/shared/tlv/test/edge_cases.proto @@ -0,0 +1,238 @@ +// Copyright 2026 Google LLC +// +// 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. + +syntax = "proto3"; + +package openprot.tlv.test.edge_cases; + +import "shared/tlv/options.proto"; + +message PackedRegister { + option (openprot.tlv.bit_packed) = "u32"; + + bool enabled = 1 [(openprot.tlv.bit_range) = "0"]; + uint32 mode = 2 [(openprot.tlv.bit_range) = "3:1"]; + uint32 status = 3 [(openprot.tlv.bit_range) = "7:4"]; + uint32 val = 4 [(openprot.tlv.bit_range) = "31:8"]; +} + +message Packed16Register { + option (openprot.tlv.bit_packed) = "u16"; + + bool flag = 1 [(openprot.tlv.bit_range) = "0"]; + uint32 mode = 2 [(openprot.tlv.bit_range) = "4:1"]; + uint32 val = 3 [(openprot.tlv.bit_range) = "15:5"]; +} + +message Packed8Register { + option (openprot.tlv.bit_packed) = "u8"; + + bool flag = 1 [(openprot.tlv.bit_range) = "0"]; + uint32 status = 2 [(openprot.tlv.bit_range) = "7:1"]; +} + +message RegisterContainer { + option (openprot.tlv.tlv_fourcc) = "CONT"; + PackedRegister reg = 1 [(openprot.tlv.offset) = 0]; + Packed16Register reg16 = 2 [(openprot.tlv.offset) = 4]; + Packed8Register reg8 = 3 [(openprot.tlv.offset) = 6]; +} + +message PackedSignedRegister { + option (openprot.tlv.bit_packed) = "u32"; + + sint32 sval = 1 [(openprot.tlv.bit_range) = "7:0"]; + uint32 uval = 2 [(openprot.tlv.bit_range) = "15:8"]; + bool flag = 3 [(openprot.tlv.bit_range) = "16"]; + sfixed32 sfix = 4 [(openprot.tlv.bit_range) = "27:17"]; +} + +message SfixedTest { + option (openprot.tlv.tlv_fourcc) = "SFIX"; + + sfixed32 sfix = 1 [(openprot.tlv.offset) = 0]; + sfixed32 sfix_i16 = 2 + [(openprot.tlv.offset) = 4, (openprot.tlv.type) = "i16"]; + sfixed32 sfix_i8 = 3 [(openprot.tlv.offset) = 6, (openprot.tlv.type) = "i8"]; +} + +// Message for testing name resolution collision +message MessageCollisionContainer { + option (openprot.tlv.tlv_fourcc) = "COLL"; + + // Nested message with the same simple name as the top-level PackedRegister + // (or similar) Let's use NestedMsg here as well, since there is no top-level + // NestedMsg in edge_cases.proto, wait! If there is no top-level NestedMsg in + // edge_cases.proto, is there a collision? Let's check: edge_cases.proto has: + // - PackedRegister (top-level) + // - PackedSignedRegister (top-level) + // - SfixedTest (top-level) + // If we name the nested message "PackedRegister" instead of "NestedMsg", then + // we will have: + // - top-level: openprot.tlv.test.edge_cases.PackedRegister + // - nested: + // openprot.tlv.test.edge_cases.MessageCollisionContainer.PackedRegister This + // is a perfect collision! + message PackedRegister { + option (openprot.tlv.tlv_fourcc) = "NPK_"; + uint32 val = 1 [(openprot.tlv.offset) = 0]; + } + + PackedRegister local = 1 [(openprot.tlv.offset) = 0]; +} + +message FloatTest { + option (openprot.tlv.tlv_fourcc) = "FLOT"; + + float fval = 1 [(openprot.tlv.offset) = 0]; + double dval = 2 [(openprot.tlv.offset) = 8]; +} + +message U64AlignmentTest { + option (openprot.tlv.tlv_fourcc) = "ALGN"; + + uint32 val32 = 1 [(openprot.tlv.offset) = 0]; + uint64 val64 = 2 [(openprot.tlv.offset) = 4]; +} + +message AllTypesTest { + option (openprot.tlv.tlv_fourcc) = "ALTS"; + + // --- 8-bit fields (offset 0..9) --- + sint32 sint32_i8 = 1 [(openprot.tlv.offset) = 0, (openprot.tlv.type) = "i8"]; + uint32 uint32_u8 = 2 [(openprot.tlv.offset) = 1, (openprot.tlv.type) = "u8"]; + int32 int32_i8 = 3 [(openprot.tlv.offset) = 2, (openprot.tlv.type) = "i8"]; + int64 int64_i8 = 4 [(openprot.tlv.offset) = 3, (openprot.tlv.type) = "i8"]; + sfixed32 sfixed32_i8 = 5 + [(openprot.tlv.offset) = 4, (openprot.tlv.type) = "i8"]; + fixed32 fixed32_u8 = 6 + [(openprot.tlv.offset) = 5, (openprot.tlv.type) = "u8"]; + sint64 sint64_i8 = 7 [(openprot.tlv.offset) = 6, (openprot.tlv.type) = "i8"]; + uint64 uint64_u8 = 8 [(openprot.tlv.offset) = 7, (openprot.tlv.type) = "u8"]; + bool bool_val = 9 [(openprot.tlv.offset) = 8, (openprot.tlv.type) = "u8"]; + + // --- 16-bit fields (offset 10..22) --- + sint32 sint32_i16 = 10 + [(openprot.tlv.offset) = 10, (openprot.tlv.type) = "i16"]; + uint32 uint32_u16 = 11 + [(openprot.tlv.offset) = 12, (openprot.tlv.type) = "u16"]; + int32 int32_i16 = 12 + [(openprot.tlv.offset) = 14, (openprot.tlv.type) = "i16"]; + int64 int64_i16 = 13 + [(openprot.tlv.offset) = 16, (openprot.tlv.type) = "i16"]; + sfixed32 sfixed32_i16 = 14 + [(openprot.tlv.offset) = 18, (openprot.tlv.type) = "i16"]; + fixed32 fixed32_u16 = 15 + [(openprot.tlv.offset) = 20, (openprot.tlv.type) = "u16"]; + + // --- 16-bit fields continued (offset 22..26) --- + sint64 sint64_i16 = 16 + [(openprot.tlv.offset) = 22, (openprot.tlv.type) = "i16"]; + uint64 uint64_u16 = 17 + [(openprot.tlv.offset) = 24, (openprot.tlv.type) = "u16"]; + + // --- 32-bit fields (offset 28..52) --- + sint32 sint32_i32 = 18 + [(openprot.tlv.offset) = 28, (openprot.tlv.type) = "i32"]; + uint32 uint32_u32 = 19 + [(openprot.tlv.offset) = 32, (openprot.tlv.type) = "u32"]; + int32 int32_i32 = 20 + [(openprot.tlv.offset) = 36, (openprot.tlv.type) = "i32"]; + sfixed32 sfixed32_i32 = 21 + [(openprot.tlv.offset) = 40, (openprot.tlv.type) = "i32"]; + fixed32 fixed32_u32 = 22 + [(openprot.tlv.offset) = 44, (openprot.tlv.type) = "u32"]; + PackedSignedRegister packed_reg = 23 [(openprot.tlv.offset) = 48]; + + // --- 32-bit fields continued / float (offset 52..68) --- + float float_val = 24 + [(openprot.tlv.offset) = 52, (openprot.tlv.type) = "f32"]; + sint64 sint64_i32 = 25 + [(openprot.tlv.offset) = 56, (openprot.tlv.type) = "i32"]; + uint64 uint64_u32 = 26 + [(openprot.tlv.offset) = 60, (openprot.tlv.type) = "u32"]; + int64 int64_i32 = 27 + [(openprot.tlv.offset) = 64, (openprot.tlv.type) = "i32"]; + + // --- 64-bit fields (offset 68..92) --- + sint64 sint64_i64 = 28 + [(openprot.tlv.offset) = 68, (openprot.tlv.type) = "i64"]; + uint64 uint64_u64 = 29 + [(openprot.tlv.offset) = 76, (openprot.tlv.type) = "u64"]; + int64 int64_i64 = 30 + [(openprot.tlv.offset) = 84, (openprot.tlv.type) = "i64"]; + + // --- 64-bit double (offset 92..100) --- + double double_val = 31 + [(openprot.tlv.offset) = 92, (openprot.tlv.type) = "f64"]; +} + +// Recursive dynamic sub-TLV message structure (binary tree node) +message Node { + option (openprot.tlv.tlv_fourcc) = "NODE"; + + uint32 value = 1 [(openprot.tlv.offset) = 0]; + repeated Node children = 2; +} + +// Verifies that cycle detection is namespace-safe. Under a simple-name cycle +// detector, the path CollisionContainer -> CollisionContainer.CollisionTest +// -> CollisionTest (top-level) would cause a false-positive circular dependency +// error. Tracking fully qualified names prevents this false positive. +message CollisionTest { + option (openprot.tlv.tlv_fourcc) = "COLT"; + uint32 value = 1 [(openprot.tlv.offset) = 0]; +} + +message CollisionContainer { + option (openprot.tlv.tlv_fourcc) = "COLC"; + + message CollisionTest { + option (openprot.tlv.tlv_fourcc) = "COLN"; + uint32 val = 1 [(openprot.tlv.offset) = 0]; + openprot.tlv.test.edge_cases.CollisionTest top_level = 2 + [(openprot.tlv.offset) = 4]; + } + + CollisionTest nested = 1 [(openprot.tlv.offset) = 0]; +} + +message SiblingInlineChild { + uint32 val = 1 [(openprot.tlv.offset) = 0]; +} + +message SiblingInlineParent { + option (openprot.tlv.tlv_fourcc) = "SIBR"; + SiblingInlineChild child1 = 1 [(openprot.tlv.offset) = 0]; + SiblingInlineChild child2 = 2 [(openprot.tlv.offset) = 4]; +} + +message RawBytesExtensionTest { + option (openprot.tlv.tlv_fourcc) = "RBXT"; + bytes payload = 1 + [(openprot.tlv.extra_data) = true, (openprot.tlv.type) = "[u8]"]; +} + +message StringExtensionTest { + option (openprot.tlv.tlv_fourcc) = "SEXT"; + string text = 1 + [(openprot.tlv.extra_data) = true, (openprot.tlv.type) = "[u8]"]; +} + +message ExtensionContainer { + option (openprot.tlv.tlv_fourcc) = "EXCT"; + uint32 id = 1 [(openprot.tlv.offset) = 0]; + RawBytesExtensionTest child_ext = 2; + RegisterContainer next_child = 3; +} diff --git a/shared/tlv/test/edge_cases_tlv.rs b/shared/tlv/test/edge_cases_tlv.rs new file mode 100644 index 00000000..34c78e26 --- /dev/null +++ b/shared/tlv/test/edge_cases_tlv.rs @@ -0,0 +1,559 @@ +// Copyright 2026 Google LLC +// +// 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. + +// Automatically generated by tlv_codegen. DO NOT EDIT. +// To regenerate, run: +// bazel run //shared/tlv/test:edge_cases_tlv_update + +#![no_std] +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] +#![allow(unused_imports)] +#![allow(dead_code)] + +use bitfield_struct::bitfield; +use openprot_tlv::TlvFlags; +use openprot_tlv::TlvHeader; +use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout}; + +#[bitfield(u32)] +#[derive(PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +pub struct PackedRegister { + #[bits(1)] + pub enabled: bool, + #[bits(3)] + pub mode: u8, + #[bits(4)] + pub status: u8, + #[bits(24)] + pub val: u32, +} + +#[bitfield(u16)] +#[derive(PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +pub struct Packed16Register { + #[bits(1)] + pub flag: bool, + #[bits(4)] + pub mode: u8, + #[bits(11)] + pub val: u16, +} + +#[bitfield(u8)] +#[derive(PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +pub struct Packed8Register { + #[bits(1)] + pub flag: bool, + #[bits(7)] + pub status: u8, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct RegisterContainer { + pub reg: PackedRegister, + pub reg16: Packed16Register, + pub reg8: Packed8Register, + pub __pad0: [u8; 1usize], +} +const _: () = { + assert!(::core::mem::offset_of!(RegisterContainer, reg) == 0usize); + assert!(::core::mem::offset_of!(RegisterContainer, reg16) == 4usize); + assert!(::core::mem::offset_of!(RegisterContainer, reg8) == 6usize); + assert!(::core::mem::align_of::() <= 4); + assert!(::core::mem::size_of::() == 8usize); +}; +impl ::core::default::Default for RegisterContainer { + fn default() -> Self { + ::zerocopy::FromZeros::new_zeroed() + } +} +impl ::openprot_tlv::TlvObject for RegisterContainer { + type Extension = [u8]; + const TAG: u32 = u32::from_le_bytes(*b"CONT"); + fn make_ext<'a>(header: &::openprot_tlv::TlvHeader, extra: &'a [u32]) -> &'a Self::Extension { + ::openprot_tlv::unpad(header, extra) + } +} + +#[bitfield(u32)] +#[derive(PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +pub struct PackedSignedRegister { + #[bits(8)] + pub sval: i8, + #[bits(8)] + pub uval: u8, + #[bits(1)] + pub flag: bool, + #[bits(11)] + pub sfix: i16, + #[bits(4)] + __: (), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct SfixedTest { + pub sfix: i32, + pub sfix_i16: i16, + pub sfix_i8: i8, + pub __pad0: [u8; 1usize], +} +const _: () = { + assert!(::core::mem::offset_of!(SfixedTest, sfix) == 0usize); + assert!(::core::mem::offset_of!(SfixedTest, sfix_i16) == 4usize); + assert!(::core::mem::offset_of!(SfixedTest, sfix_i8) == 6usize); + assert!(::core::mem::align_of::() <= 4); + assert!(::core::mem::size_of::() == 8usize); +}; +impl ::core::default::Default for SfixedTest { + fn default() -> Self { + ::zerocopy::FromZeros::new_zeroed() + } +} +impl ::openprot_tlv::TlvObject for SfixedTest { + type Extension = [u8]; + const TAG: u32 = u32::from_le_bytes(*b"SFIX"); + fn make_ext<'a>(header: &::openprot_tlv::TlvHeader, extra: &'a [u32]) -> &'a Self::Extension { + ::openprot_tlv::unpad(header, extra) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct MessageCollisionContainer { + pub local: MessageCollisionContainer_PackedRegister, +} +const _: () = { + assert!(::core::mem::offset_of!(MessageCollisionContainer, local) == 0usize); + assert!(::core::mem::align_of::() <= 4); + assert!(::core::mem::size_of::() == 4usize); +}; +impl ::core::default::Default for MessageCollisionContainer { + fn default() -> Self { + ::zerocopy::FromZeros::new_zeroed() + } +} +impl ::openprot_tlv::TlvObject for MessageCollisionContainer { + type Extension = [u8]; + const TAG: u32 = u32::from_le_bytes(*b"COLL"); + fn make_ext<'a>(header: &::openprot_tlv::TlvHeader, extra: &'a [u32]) -> &'a Self::Extension { + ::openprot_tlv::unpad(header, extra) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct MessageCollisionContainer_PackedRegister { + pub val: u32, +} +const _: () = { + assert!(::core::mem::offset_of!(MessageCollisionContainer_PackedRegister, val) == 0usize); + assert!(::core::mem::align_of::() <= 4); + assert!(::core::mem::size_of::() == 4usize); +}; +impl ::core::default::Default for MessageCollisionContainer_PackedRegister { + fn default() -> Self { + ::zerocopy::FromZeros::new_zeroed() + } +} +impl ::openprot_tlv::TlvObject for MessageCollisionContainer_PackedRegister { + type Extension = [u8]; + const TAG: u32 = u32::from_le_bytes(*b"NPK_"); + fn make_ext<'a>(header: &::openprot_tlv::TlvHeader, extra: &'a [u32]) -> &'a Self::Extension { + ::openprot_tlv::unpad(header, extra) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct FloatTest { + pub fval: f32, + pub __pad0: [u8; 4usize], + pub dval: ::zerocopy::byteorder::native_endian::F64, +} +const _: () = { + assert!(::core::mem::offset_of!(FloatTest, fval) == 0usize); + assert!(::core::mem::offset_of!(FloatTest, dval) == 8usize); + assert!(::core::mem::align_of::() <= 4); + assert!(::core::mem::size_of::() == 16usize); +}; +impl ::core::default::Default for FloatTest { + fn default() -> Self { + ::zerocopy::FromZeros::new_zeroed() + } +} +impl ::openprot_tlv::TlvObject for FloatTest { + type Extension = [u8]; + const TAG: u32 = u32::from_le_bytes(*b"FLOT"); + fn make_ext<'a>(header: &::openprot_tlv::TlvHeader, extra: &'a [u32]) -> &'a Self::Extension { + ::openprot_tlv::unpad(header, extra) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct U64AlignmentTest { + pub val32: u32, + pub val64: ::openprot_aligned::U64Align4, +} +const _: () = { + assert!(::core::mem::offset_of!(U64AlignmentTest, val32) == 0usize); + assert!(::core::mem::offset_of!(U64AlignmentTest, val64) == 4usize); + assert!(::core::mem::align_of::() <= 4); + assert!(::core::mem::size_of::() == 12usize); +}; +impl ::core::default::Default for U64AlignmentTest { + fn default() -> Self { + ::zerocopy::FromZeros::new_zeroed() + } +} +impl ::openprot_tlv::TlvObject for U64AlignmentTest { + type Extension = [u8]; + const TAG: u32 = u32::from_le_bytes(*b"ALGN"); + fn make_ext<'a>(header: &::openprot_tlv::TlvHeader, extra: &'a [u32]) -> &'a Self::Extension { + ::openprot_tlv::unpad(header, extra) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct AllTypesTest { + pub sint32_i8: i8, + pub uint32_u8: u8, + pub int32_i8: i8, + pub int64_i8: i8, + pub sfixed32_i8: i8, + pub fixed32_u8: u8, + pub sint64_i8: i8, + pub uint64_u8: u8, + pub bool_val: u8, + pub __pad0: [u8; 1usize], + pub sint32_i16: i16, + pub uint32_u16: u16, + pub int32_i16: i16, + pub int64_i16: i16, + pub sfixed32_i16: i16, + pub fixed32_u16: u16, + pub sint64_i16: i16, + pub uint64_u16: u16, + pub __pad1: [u8; 2usize], + pub sint32_i32: i32, + pub uint32_u32: u32, + pub int32_i32: i32, + pub sfixed32_i32: i32, + pub fixed32_u32: u32, + pub packed_reg: PackedSignedRegister, + pub float_val: f32, + pub sint64_i32: i32, + pub uint64_u32: u32, + pub int64_i32: i32, + pub sint64_i64: ::zerocopy::byteorder::native_endian::I64, + pub uint64_u64: ::openprot_aligned::U64Align4, + pub int64_i64: ::zerocopy::byteorder::native_endian::I64, + pub double_val: ::zerocopy::byteorder::native_endian::F64, +} +const _: () = { + assert!(::core::mem::offset_of!(AllTypesTest, sint32_i8) == 0usize); + assert!(::core::mem::offset_of!(AllTypesTest, uint32_u8) == 1usize); + assert!(::core::mem::offset_of!(AllTypesTest, int32_i8) == 2usize); + assert!(::core::mem::offset_of!(AllTypesTest, int64_i8) == 3usize); + assert!(::core::mem::offset_of!(AllTypesTest, sfixed32_i8) == 4usize); + assert!(::core::mem::offset_of!(AllTypesTest, fixed32_u8) == 5usize); + assert!(::core::mem::offset_of!(AllTypesTest, sint64_i8) == 6usize); + assert!(::core::mem::offset_of!(AllTypesTest, uint64_u8) == 7usize); + assert!(::core::mem::offset_of!(AllTypesTest, bool_val) == 8usize); + assert!(::core::mem::offset_of!(AllTypesTest, sint32_i16) == 10usize); + assert!(::core::mem::offset_of!(AllTypesTest, uint32_u16) == 12usize); + assert!(::core::mem::offset_of!(AllTypesTest, int32_i16) == 14usize); + assert!(::core::mem::offset_of!(AllTypesTest, int64_i16) == 16usize); + assert!(::core::mem::offset_of!(AllTypesTest, sfixed32_i16) == 18usize); + assert!(::core::mem::offset_of!(AllTypesTest, fixed32_u16) == 20usize); + assert!(::core::mem::offset_of!(AllTypesTest, sint64_i16) == 22usize); + assert!(::core::mem::offset_of!(AllTypesTest, uint64_u16) == 24usize); + assert!(::core::mem::offset_of!(AllTypesTest, sint32_i32) == 28usize); + assert!(::core::mem::offset_of!(AllTypesTest, uint32_u32) == 32usize); + assert!(::core::mem::offset_of!(AllTypesTest, int32_i32) == 36usize); + assert!(::core::mem::offset_of!(AllTypesTest, sfixed32_i32) == 40usize); + assert!(::core::mem::offset_of!(AllTypesTest, fixed32_u32) == 44usize); + assert!(::core::mem::offset_of!(AllTypesTest, packed_reg) == 48usize); + assert!(::core::mem::offset_of!(AllTypesTest, float_val) == 52usize); + assert!(::core::mem::offset_of!(AllTypesTest, sint64_i32) == 56usize); + assert!(::core::mem::offset_of!(AllTypesTest, uint64_u32) == 60usize); + assert!(::core::mem::offset_of!(AllTypesTest, int64_i32) == 64usize); + assert!(::core::mem::offset_of!(AllTypesTest, sint64_i64) == 68usize); + assert!(::core::mem::offset_of!(AllTypesTest, uint64_u64) == 76usize); + assert!(::core::mem::offset_of!(AllTypesTest, int64_i64) == 84usize); + assert!(::core::mem::offset_of!(AllTypesTest, double_val) == 92usize); + assert!(::core::mem::align_of::() <= 4); + assert!(::core::mem::size_of::() == 100usize); +}; +impl ::core::default::Default for AllTypesTest { + fn default() -> Self { + ::zerocopy::FromZeros::new_zeroed() + } +} +impl ::openprot_tlv::TlvObject for AllTypesTest { + type Extension = [u8]; + const TAG: u32 = u32::from_le_bytes(*b"ALTS"); + fn make_ext<'a>(header: &::openprot_tlv::TlvHeader, extra: &'a [u32]) -> &'a Self::Extension { + ::openprot_tlv::unpad(header, extra) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct Node { + pub value: u32, +} +const _: () = { + assert!(::core::mem::offset_of!(Node, value) == 0usize); + assert!(::core::mem::align_of::() <= 4); + assert!(::core::mem::size_of::() == 4usize); +}; +impl ::core::default::Default for Node { + fn default() -> Self { + ::zerocopy::FromZeros::new_zeroed() + } +} +impl ::openprot_tlv::TlvObject for Node { + type Extension = ::openprot_tlv::TlvData; + const TAG: u32 = u32::from_le_bytes(*b"NODE"); + fn make_ext<'a>(_header: &::openprot_tlv::TlvHeader, extra: &'a [u32]) -> &'a Self::Extension { + ::openprot_tlv::TlvData::overlay(extra) + } +} +pub trait NodeExt<'a> { + fn children(&self) -> ::openprot_tlv::TlvIterator<'a, Node>; +} +impl<'a> NodeExt<'a> for ::openprot_tlv::TlvItem<'a, Node> { + #[inline(always)] + fn children(&self) -> ::openprot_tlv::TlvIterator<'a, Node> { + ::openprot_tlv::TlvQuery::ext(self).iter::() + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct CollisionTest { + pub value: u32, +} +const _: () = { + assert!(::core::mem::offset_of!(CollisionTest, value) == 0usize); + assert!(::core::mem::align_of::() <= 4); + assert!(::core::mem::size_of::() == 4usize); +}; +impl ::core::default::Default for CollisionTest { + fn default() -> Self { + ::zerocopy::FromZeros::new_zeroed() + } +} +impl ::openprot_tlv::TlvObject for CollisionTest { + type Extension = [u8]; + const TAG: u32 = u32::from_le_bytes(*b"COLT"); + fn make_ext<'a>(header: &::openprot_tlv::TlvHeader, extra: &'a [u32]) -> &'a Self::Extension { + ::openprot_tlv::unpad(header, extra) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct CollisionContainer { + pub nested: CollisionContainer_CollisionTest, +} +const _: () = { + assert!(::core::mem::offset_of!(CollisionContainer, nested) == 0usize); + assert!(::core::mem::align_of::() <= 4); + assert!(::core::mem::size_of::() == 8usize); +}; +impl ::core::default::Default for CollisionContainer { + fn default() -> Self { + ::zerocopy::FromZeros::new_zeroed() + } +} +impl ::openprot_tlv::TlvObject for CollisionContainer { + type Extension = [u8]; + const TAG: u32 = u32::from_le_bytes(*b"COLC"); + fn make_ext<'a>(header: &::openprot_tlv::TlvHeader, extra: &'a [u32]) -> &'a Self::Extension { + ::openprot_tlv::unpad(header, extra) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct CollisionContainer_CollisionTest { + pub val: u32, + pub top_level: CollisionTest, +} +const _: () = { + assert!(::core::mem::offset_of!(CollisionContainer_CollisionTest, val) == 0usize); + assert!(::core::mem::offset_of!(CollisionContainer_CollisionTest, top_level) == 4usize); + assert!(::core::mem::align_of::() <= 4); + assert!(::core::mem::size_of::() == 8usize); +}; +impl ::core::default::Default for CollisionContainer_CollisionTest { + fn default() -> Self { + ::zerocopy::FromZeros::new_zeroed() + } +} +impl ::openprot_tlv::TlvObject for CollisionContainer_CollisionTest { + type Extension = [u8]; + const TAG: u32 = u32::from_le_bytes(*b"COLN"); + fn make_ext<'a>(header: &::openprot_tlv::TlvHeader, extra: &'a [u32]) -> &'a Self::Extension { + ::openprot_tlv::unpad(header, extra) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct SiblingInlineChild { + pub val: u32, +} +const _: () = { + assert!(::core::mem::offset_of!(SiblingInlineChild, val) == 0usize); + assert!(::core::mem::align_of::() <= 4); + assert!(::core::mem::size_of::() == 4usize); +}; +impl ::core::default::Default for SiblingInlineChild { + fn default() -> Self { + ::zerocopy::FromZeros::new_zeroed() + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct SiblingInlineParent { + pub child1: SiblingInlineChild, + pub child2: SiblingInlineChild, +} +const _: () = { + assert!(::core::mem::offset_of!(SiblingInlineParent, child1) == 0usize); + assert!(::core::mem::offset_of!(SiblingInlineParent, child2) == 4usize); + assert!(::core::mem::align_of::() <= 4); + assert!(::core::mem::size_of::() == 8usize); +}; +impl ::core::default::Default for SiblingInlineParent { + fn default() -> Self { + ::zerocopy::FromZeros::new_zeroed() + } +} +impl ::openprot_tlv::TlvObject for SiblingInlineParent { + type Extension = [u8]; + const TAG: u32 = u32::from_le_bytes(*b"SIBR"); + fn make_ext<'a>(header: &::openprot_tlv::TlvHeader, extra: &'a [u32]) -> &'a Self::Extension { + ::openprot_tlv::unpad(header, extra) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct RawBytesExtensionTest {} +const _: () = { + assert!(::core::mem::align_of::() <= 4); + assert!(::core::mem::size_of::() == 0usize); +}; +impl ::core::default::Default for RawBytesExtensionTest { + fn default() -> Self { + ::zerocopy::FromZeros::new_zeroed() + } +} +impl ::openprot_tlv::TlvObject for RawBytesExtensionTest { + type Extension = [u8]; + const TAG: u32 = u32::from_le_bytes(*b"RBXT"); + fn make_ext<'a>(header: &::openprot_tlv::TlvHeader, extra: &'a [u32]) -> &'a Self::Extension { + ::openprot_tlv::unpad(header, extra) + } +} +pub trait RawBytesExtensionTestExt<'a> { + fn payload(&self) -> &'a [u8]; +} +impl<'a> RawBytesExtensionTestExt<'a> for ::openprot_tlv::TlvItem<'a, RawBytesExtensionTest> { + #[inline(always)] + fn payload(&self) -> &'a [u8] { + use ::openprot_tlv::TlvQuery; + self.ext() + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct StringExtensionTest {} +const _: () = { + assert!(::core::mem::align_of::() <= 4); + assert!(::core::mem::size_of::() == 0usize); +}; +impl ::core::default::Default for StringExtensionTest { + fn default() -> Self { + ::zerocopy::FromZeros::new_zeroed() + } +} +impl ::openprot_tlv::TlvObject for StringExtensionTest { + type Extension = [u8]; + const TAG: u32 = u32::from_le_bytes(*b"SEXT"); + fn make_ext<'a>(header: &::openprot_tlv::TlvHeader, extra: &'a [u32]) -> &'a Self::Extension { + ::openprot_tlv::unpad(header, extra) + } +} +pub trait StringExtensionTestExt<'a> { + fn text(&self) -> &'a ::bstr::BStr; +} +impl<'a> StringExtensionTestExt<'a> for ::openprot_tlv::TlvItem<'a, StringExtensionTest> { + #[inline(always)] + fn text(&self) -> &'a ::bstr::BStr { + use ::openprot_tlv::TlvQuery; + ::bstr::BStr::new(self.ext()) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct ExtensionContainer { + pub id: u32, +} +const _: () = { + assert!(::core::mem::offset_of!(ExtensionContainer, id) == 0usize); + assert!(::core::mem::align_of::() <= 4); + assert!(::core::mem::size_of::() == 4usize); +}; +impl ::core::default::Default for ExtensionContainer { + fn default() -> Self { + ::zerocopy::FromZeros::new_zeroed() + } +} +impl ::openprot_tlv::TlvObject for ExtensionContainer { + type Extension = ::openprot_tlv::TlvData; + const TAG: u32 = u32::from_le_bytes(*b"EXCT"); + fn make_ext<'a>(_header: &::openprot_tlv::TlvHeader, extra: &'a [u32]) -> &'a Self::Extension { + ::openprot_tlv::TlvData::overlay(extra) + } +} +pub trait ExtensionContainerExt<'a> { + fn child_ext(&self) -> Option<::openprot_tlv::TlvItem<'a, RawBytesExtensionTest>>; + fn next_child(&self) -> Option<::openprot_tlv::TlvItem<'a, RegisterContainer>>; +} +impl<'a> ExtensionContainerExt<'a> for ::openprot_tlv::TlvItem<'a, ExtensionContainer> { + #[inline(always)] + fn child_ext(&self) -> Option<::openprot_tlv::TlvItem<'a, RawBytesExtensionTest>> { + ::openprot_tlv::TlvQuery::ext(self) + .iter::() + .next() + } + #[inline(always)] + fn next_child(&self) -> Option<::openprot_tlv::TlvItem<'a, RegisterContainer>> { + ::openprot_tlv::TlvQuery::ext(self) + .iter::() + .next() + } +} diff --git a/shared/tlv/test/import_base.proto b/shared/tlv/test/import_base.proto new file mode 100644 index 00000000..2425a0e2 --- /dev/null +++ b/shared/tlv/test/import_base.proto @@ -0,0 +1,34 @@ +// Copyright 2026 Google LLC +// +// 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. + +syntax = "proto3"; + +package openprot.tlv.test.imports.base; + +import "shared/tlv/options.proto"; + +enum BaseStatus { + option (openprot.tlv.raw_type) = "u8"; + + BASE_STATUS_UNKNOWN = 0; + BASE_STATUS_OK = 1; + BASE_STATUS_ERROR = 2; +} + +message BaseRecord { + option (openprot.tlv.tlv_fourcc) = "BREC"; + + uint32 id = 1 [(openprot.tlv.offset) = 0]; + BaseStatus status = 2 [(openprot.tlv.offset) = 4]; +} diff --git a/shared/tlv/test/import_base_tlv.rs b/shared/tlv/test/import_base_tlv.rs new file mode 100644 index 00000000..a16c6943 --- /dev/null +++ b/shared/tlv/test/import_base_tlv.rs @@ -0,0 +1,75 @@ +// Copyright 2026 Google LLC +// +// 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. + +// Automatically generated by tlv_codegen. DO NOT EDIT. +// To regenerate, run: +// bazel run //shared/tlv/test:import_base_tlv_update + +#![no_std] +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] +#![allow(unused_imports)] +#![allow(dead_code)] + +use bitfield_struct::bitfield; +use openprot_tlv::TlvFlags; +use openprot_tlv::TlvHeader; +use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout}; + +#[derive( + Clone, Copy, PartialEq, Eq, Hash, Default, FromBytes, IntoBytes, Immutable, KnownLayout, +)] +#[repr(transparent)] +pub struct BaseStatus(pub u8); +impl BaseStatus { + pub const UNKNOWN: Self = Self(0); + pub const OK: Self = Self(1); + pub const ERROR: Self = Self(2); +} +impl ::core::fmt::Debug for BaseStatus { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + match *self { + Self::UNKNOWN => write!(f, "UNKNOWN"), + Self::OK => write!(f, "OK"), + Self::ERROR => write!(f, "ERROR"), + other => write!(f, "{}({})", stringify!(BaseStatus), other.0), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct BaseRecord { + pub id: u32, + pub status: BaseStatus, + pub __pad0: [u8; 3usize], +} +const _: () = { + assert!(::core::mem::offset_of!(BaseRecord, id) == 0usize); + assert!(::core::mem::offset_of!(BaseRecord, status) == 4usize); + assert!(::core::mem::align_of::() <= 4); + assert!(::core::mem::size_of::() == 8usize); +}; +impl ::core::default::Default for BaseRecord { + fn default() -> Self { + ::zerocopy::FromZeros::new_zeroed() + } +} +impl ::openprot_tlv::TlvObject for BaseRecord { + type Extension = [u8]; + const TAG: u32 = u32::from_le_bytes(*b"BREC"); + fn make_ext<'a>(header: &::openprot_tlv::TlvHeader, extra: &'a [u32]) -> &'a Self::Extension { + ::openprot_tlv::unpad(header, extra) + } +} diff --git a/shared/tlv/test/import_layered.proto b/shared/tlv/test/import_layered.proto new file mode 100644 index 00000000..8ad3f199 --- /dev/null +++ b/shared/tlv/test/import_layered.proto @@ -0,0 +1,31 @@ +// Copyright 2026 Google LLC +// +// 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. + +syntax = "proto3"; + +package openprot.tlv.test.imports.layered; + +import "shared/tlv/options.proto"; +import "shared/tlv/test/import_base.proto"; +import "shared/tlv/test/import_simple.proto"; + +message LayeredMessage { + option (openprot.tlv.tlv_fourcc) = "LAYR"; + + uint32 layer_id = 1 [(openprot.tlv.offset) = 0]; + + // Extension items from both simple and base crates + openprot.tlv.test.imports.simple.SimpleMessage simple = 2; + openprot.tlv.test.imports.base.BaseRecord base = 3; +} diff --git a/shared/tlv/test/import_layered_tlv.rs b/shared/tlv/test/import_layered_tlv.rs new file mode 100644 index 00000000..ee25dc52 --- /dev/null +++ b/shared/tlv/test/import_layered_tlv.rs @@ -0,0 +1,71 @@ +// Copyright 2026 Google LLC +// +// 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. + +// Automatically generated by tlv_codegen. DO NOT EDIT. +// To regenerate, run: +// bazel run //shared/tlv/test:import_layered_tlv_update + +#![no_std] +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] +#![allow(unused_imports)] +#![allow(dead_code)] + +use bitfield_struct::bitfield; +pub use import_base_tlv::*; +pub use import_simple_tlv::*; +use openprot_tlv::TlvFlags; +use openprot_tlv::TlvHeader; +use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct LayeredMessage { + pub layer_id: u32, +} +const _: () = { + assert!(::core::mem::offset_of!(LayeredMessage, layer_id) == 0usize); + assert!(::core::mem::align_of::() <= 4); + assert!(::core::mem::size_of::() == 4usize); +}; +impl ::core::default::Default for LayeredMessage { + fn default() -> Self { + ::zerocopy::FromZeros::new_zeroed() + } +} +impl ::openprot_tlv::TlvObject for LayeredMessage { + type Extension = ::openprot_tlv::TlvData; + const TAG: u32 = u32::from_le_bytes(*b"LAYR"); + fn make_ext<'a>(_header: &::openprot_tlv::TlvHeader, extra: &'a [u32]) -> &'a Self::Extension { + ::openprot_tlv::TlvData::overlay(extra) + } +} +pub trait LayeredMessageExt<'a> { + fn simple(&self) -> Option<::openprot_tlv::TlvItem<'a, SimpleMessage>>; + fn base(&self) -> Option<::openprot_tlv::TlvItem<'a, BaseRecord>>; +} +impl<'a> LayeredMessageExt<'a> for ::openprot_tlv::TlvItem<'a, LayeredMessage> { + #[inline(always)] + fn simple(&self) -> Option<::openprot_tlv::TlvItem<'a, SimpleMessage>> { + ::openprot_tlv::TlvQuery::ext(self) + .iter::() + .next() + } + #[inline(always)] + fn base(&self) -> Option<::openprot_tlv::TlvItem<'a, BaseRecord>> { + ::openprot_tlv::TlvQuery::ext(self) + .iter::() + .next() + } +} diff --git a/shared/tlv/test/import_pure_tlv.rs b/shared/tlv/test/import_pure_tlv.rs new file mode 100644 index 00000000..2b7f2e4c --- /dev/null +++ b/shared/tlv/test/import_pure_tlv.rs @@ -0,0 +1,31 @@ +// Copyright 2026 Google LLC +// +// 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. + +// Automatically generated by tlv_codegen. DO NOT EDIT. +// To regenerate, run: +// bazel run //shared/tlv/test:import_pure_tlv_update + +#![no_std] +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] +#![allow(unused_imports)] +#![allow(dead_code)] + +use bitfield_struct::bitfield; +pub use import_base_tlv::*; +pub use import_layered_tlv::*; +pub use import_simple_tlv::*; +use openprot_tlv::TlvFlags; +use openprot_tlv::TlvHeader; +use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout}; diff --git a/shared/tlv/test/import_simple.proto b/shared/tlv/test/import_simple.proto new file mode 100644 index 00000000..622c67b0 --- /dev/null +++ b/shared/tlv/test/import_simple.proto @@ -0,0 +1,31 @@ +// Copyright 2026 Google LLC +// +// 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. + +syntax = "proto3"; + +package openprot.tlv.test.imports.simple; + +import "shared/tlv/options.proto"; +import "shared/tlv/test/import_base.proto"; + +message SimpleMessage { + option (openprot.tlv.tlv_fourcc) = "SMPL"; + + uint32 count = 1 [(openprot.tlv.offset) = 0]; + openprot.tlv.test.imports.base.BaseStatus status = 2 + [(openprot.tlv.offset) = 4]; + + // Nested/extension item referencing BaseRecord from imported base crate + openprot.tlv.test.imports.base.BaseRecord record = 3; +} diff --git a/shared/tlv/test/import_simple_tlv.rs b/shared/tlv/test/import_simple_tlv.rs new file mode 100644 index 00000000..405a451c --- /dev/null +++ b/shared/tlv/test/import_simple_tlv.rs @@ -0,0 +1,66 @@ +// Copyright 2026 Google LLC +// +// 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. + +// Automatically generated by tlv_codegen. DO NOT EDIT. +// To regenerate, run: +// bazel run //shared/tlv/test:import_simple_tlv_update + +#![no_std] +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] +#![allow(unused_imports)] +#![allow(dead_code)] + +use bitfield_struct::bitfield; +pub use import_base_tlv::*; +use openprot_tlv::TlvFlags; +use openprot_tlv::TlvHeader; +use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct SimpleMessage { + pub count: u32, + pub status: BaseStatus, + pub __pad0: [u8; 3usize], +} +const _: () = { + assert!(::core::mem::offset_of!(SimpleMessage, count) == 0usize); + assert!(::core::mem::offset_of!(SimpleMessage, status) == 4usize); + assert!(::core::mem::align_of::() <= 4); + assert!(::core::mem::size_of::() == 8usize); +}; +impl ::core::default::Default for SimpleMessage { + fn default() -> Self { + ::zerocopy::FromZeros::new_zeroed() + } +} +impl ::openprot_tlv::TlvObject for SimpleMessage { + type Extension = ::openprot_tlv::TlvData; + const TAG: u32 = u32::from_le_bytes(*b"SMPL"); + fn make_ext<'a>(_header: &::openprot_tlv::TlvHeader, extra: &'a [u32]) -> &'a Self::Extension { + ::openprot_tlv::TlvData::overlay(extra) + } +} +pub trait SimpleMessageExt<'a> { + fn record(&self) -> Option<::openprot_tlv::TlvItem<'a, BaseRecord>>; +} +impl<'a> SimpleMessageExt<'a> for ::openprot_tlv::TlvItem<'a, SimpleMessage> { + #[inline(always)] + fn record(&self) -> Option<::openprot_tlv::TlvItem<'a, BaseRecord>> { + ::openprot_tlv::TlvQuery::ext(self) + .iter::() + .next() + } +} diff --git a/shared/tlv/test/soc.proto b/shared/tlv/test/soc.proto new file mode 100644 index 00000000..1a8a5d01 --- /dev/null +++ b/shared/tlv/test/soc.proto @@ -0,0 +1,138 @@ +// Copyright 2026 Google LLC +// +// 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. + +syntax = "proto3"; + +package openprot.tlv.test.soc; + +import "shared/tlv/options.proto"; + +message System { + option (openprot.tlv.tlv_fourcc) = "SYS_"; + + uint32 vendor_id = 1 [(openprot.tlv.offset) = 0]; + uint32 product_id = 2 [(openprot.tlv.offset) = 4]; + uint32 total_mem_mib = 3 [(openprot.tlv.offset) = 8]; + + // Inline fixed-size string buffer (16 bytes) + string system_name = 6 + [(openprot.tlv.offset) = 12, (openprot.tlv.type) = "[u8; 16]"]; + + // Inline fixed-size byte array (16 bytes) + bytes system_uuid = 7 + [(openprot.tlv.offset) = 28, (openprot.tlv.type) = "[u32; 4]"]; + + repeated CpuCore cores = 4; + SystemBus bus = 5; + repeated Accelerator accelerators = 8; +} + +message CoreIdentifier { + uint32 model_id = 1 [(openprot.tlv.offset) = 0]; + uint32 revision = 2 [(openprot.tlv.offset) = 4]; +} + +message CpuCore { + option (openprot.tlv.tlv_fourcc) = "CORE"; + + uint32 base_frequency_mhz = 1 [(openprot.tlv.offset) = 0]; + + CoreIdentifier id = 4 [(openprot.tlv.offset) = 4]; + + CacheInfo cache = 2; + repeated ExecutionUnit execution_units = 3; +} + +message BusFeatures { + option (openprot.tlv.bit_packed) = "u8"; + + bool is_primary = 1 [(openprot.tlv.bit_range) = "0"]; + bool supports_dma = 2 [(openprot.tlv.bit_range) = "1"]; + bool supports_coherent = 3 [(openprot.tlv.bit_range) = "2"]; + uint32 burst_len_log2 = 4 [(openprot.tlv.bit_range) = "7:3"]; +} + +message SystemBus { + option (openprot.tlv.tlv_fourcc) = "SBUS"; + + uint32 bus_type = 1 [(openprot.tlv.offset) = 0]; + uint32 width_bits = 2 [(openprot.tlv.offset) = 4]; + uint32 speed_mhz = 3 [(openprot.tlv.offset) = 8]; + + BusFeatures features = 4 [(openprot.tlv.offset) = 12]; +} + +message CacheInfo { + option (openprot.tlv.tlv_fourcc) = "CACH"; + + uint32 level = 1 [(openprot.tlv.offset) = 0]; + uint32 size_kib = 2 [(openprot.tlv.offset) = 4]; + uint32 associativity = 3 [(openprot.tlv.offset) = 8]; +} + +enum ExecutionUnitState { + option (openprot.tlv.raw_type) = "u8"; + option allow_alias = true; + + EU_STATE_IDLE = 0; + EU_STATE_BUSY = 1; + EU_STATE_STALLED = 2; + EU_STATE_LARGE = 150; + EU_STATE_ALIAS = 0; // Alias for IDLE to test duplicate variant values! +} + +message ExecutionUnit { + option (openprot.tlv.tlv_fourcc) = "EXEU"; + + // Pack as 1 byte at offset 0 + uint32 unit_type = 1 [(openprot.tlv.offset) = 0, (openprot.tlv.type) = "u8"]; + + // Pack as 1 byte at offset 1 + uint32 pipeline_stages = 2 + [(openprot.tlv.offset) = 1, (openprot.tlv.type) = "u8"]; + + // Pack an enum at offset 2. Backing size is resolved automatically from + // raw_type! + ExecutionUnitState state = 3 [(openprot.tlv.offset) = 2]; + + // Pack as 2 bytes at offset 4 + uint32 power_mw = 4 [(openprot.tlv.offset) = 4, (openprot.tlv.type) = "u16"]; +} + +message GpuAccelerator { + option (openprot.tlv.tlv_fourcc) = "GPU_"; + uint32 cores = 1 [(openprot.tlv.offset) = 0]; + uint32 frequency_mhz = 2 [(openprot.tlv.offset) = 4]; +} + +message NpuAccelerator { + option (openprot.tlv.tlv_fourcc) = "NPU_"; + uint32 tops = 1 [(openprot.tlv.offset) = 0]; +} + +message CryptoAccelerator { + option (openprot.tlv.tlv_fourcc) = "CRYP"; + uint32 key_size_bits = 1 [(openprot.tlv.offset) = 0]; + bytes supported_ciphers = 2 + [(openprot.tlv.extra_data) = true, (openprot.tlv.type) = "[u8]"]; +} + +message Accelerator { + option (openprot.tlv.tlv_fourcc) = "ACCL"; + oneof config { + GpuAccelerator gpu = 1; + NpuAccelerator npu = 2; + CryptoAccelerator crypto = 3; + } +} diff --git a/shared/tlv/test/soc_tlv.rs b/shared/tlv/test/soc_tlv.rs new file mode 100755 index 00000000..e2660fe0 --- /dev/null +++ b/shared/tlv/test/soc_tlv.rs @@ -0,0 +1,412 @@ +// Copyright 2026 Google LLC +// +// 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. + +// Automatically generated by tlv_codegen. DO NOT EDIT. +// To regenerate, run: +// bazel run //shared/tlv/test:soc_tlv_update + +#![no_std] +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] +#![allow(unused_imports)] +#![allow(dead_code)] + +use bitfield_struct::bitfield; +use openprot_tlv::TlvFlags; +use openprot_tlv::TlvHeader; +use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout}; + +#[derive( + Clone, Copy, PartialEq, Eq, Hash, Default, FromBytes, IntoBytes, Immutable, KnownLayout, +)] +#[repr(transparent)] +pub struct ExecutionUnitState(pub u8); +impl ExecutionUnitState { + pub const IDLE: Self = Self(0); + pub const BUSY: Self = Self(1); + pub const STALLED: Self = Self(2); + pub const LARGE: Self = Self(150); + pub const ALIAS: Self = Self(0); +} +impl ::core::fmt::Debug for ExecutionUnitState { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + match *self { + Self::IDLE => write!(f, "IDLE"), + Self::BUSY => write!(f, "BUSY"), + Self::STALLED => write!(f, "STALLED"), + Self::LARGE => write!(f, "LARGE"), + other => write!(f, "{}({})", stringify!(ExecutionUnitState), other.0), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct System { + pub vendor_id: u32, + pub product_id: u32, + pub total_mem_mib: u32, + pub system_name: [u8; 16], + pub system_uuid: [u32; 4], +} +const _: () = { + assert!(::core::mem::offset_of!(System, vendor_id) == 0usize); + assert!(::core::mem::offset_of!(System, product_id) == 4usize); + assert!(::core::mem::offset_of!(System, total_mem_mib) == 8usize); + assert!(::core::mem::offset_of!(System, system_name) == 12usize); + assert!(::core::mem::offset_of!(System, system_uuid) == 28usize); + assert!(::core::mem::align_of::() <= 4); + assert!(::core::mem::size_of::() == 44usize); +}; +impl ::core::default::Default for System { + fn default() -> Self { + ::zerocopy::FromZeros::new_zeroed() + } +} +impl ::openprot_tlv::TlvObject for System { + type Extension = ::openprot_tlv::TlvData; + const TAG: u32 = u32::from_le_bytes(*b"SYS_"); + fn make_ext<'a>(_header: &::openprot_tlv::TlvHeader, extra: &'a [u32]) -> &'a Self::Extension { + ::openprot_tlv::TlvData::overlay(extra) + } +} +pub trait SystemExt<'a> { + fn cores(&self) -> ::openprot_tlv::TlvIterator<'a, CpuCore>; + fn bus(&self) -> Option<::openprot_tlv::TlvItem<'a, SystemBus>>; + fn accelerators(&self) -> ::openprot_tlv::TlvIterator<'a, Accelerator>; +} +impl<'a> SystemExt<'a> for ::openprot_tlv::TlvItem<'a, System> { + #[inline(always)] + fn cores(&self) -> ::openprot_tlv::TlvIterator<'a, CpuCore> { + ::openprot_tlv::TlvQuery::ext(self).iter::() + } + #[inline(always)] + fn bus(&self) -> Option<::openprot_tlv::TlvItem<'a, SystemBus>> { + ::openprot_tlv::TlvQuery::ext(self) + .iter::() + .next() + } + #[inline(always)] + fn accelerators(&self) -> ::openprot_tlv::TlvIterator<'a, Accelerator> { + ::openprot_tlv::TlvQuery::ext(self).iter::() + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct CoreIdentifier { + pub model_id: u32, + pub revision: u32, +} +const _: () = { + assert!(::core::mem::offset_of!(CoreIdentifier, model_id) == 0usize); + assert!(::core::mem::offset_of!(CoreIdentifier, revision) == 4usize); + assert!(::core::mem::align_of::() <= 4); + assert!(::core::mem::size_of::() == 8usize); +}; +impl ::core::default::Default for CoreIdentifier { + fn default() -> Self { + ::zerocopy::FromZeros::new_zeroed() + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct CpuCore { + pub base_frequency_mhz: u32, + pub id: CoreIdentifier, +} +const _: () = { + assert!(::core::mem::offset_of!(CpuCore, base_frequency_mhz) == 0usize); + assert!(::core::mem::offset_of!(CpuCore, id) == 4usize); + assert!(::core::mem::align_of::() <= 4); + assert!(::core::mem::size_of::() == 12usize); +}; +impl ::core::default::Default for CpuCore { + fn default() -> Self { + ::zerocopy::FromZeros::new_zeroed() + } +} +impl ::openprot_tlv::TlvObject for CpuCore { + type Extension = ::openprot_tlv::TlvData; + const TAG: u32 = u32::from_le_bytes(*b"CORE"); + fn make_ext<'a>(_header: &::openprot_tlv::TlvHeader, extra: &'a [u32]) -> &'a Self::Extension { + ::openprot_tlv::TlvData::overlay(extra) + } +} +pub trait CpuCoreExt<'a> { + fn cache(&self) -> Option<::openprot_tlv::TlvItem<'a, CacheInfo>>; + fn execution_units(&self) -> ::openprot_tlv::TlvIterator<'a, ExecutionUnit>; +} +impl<'a> CpuCoreExt<'a> for ::openprot_tlv::TlvItem<'a, CpuCore> { + #[inline(always)] + fn cache(&self) -> Option<::openprot_tlv::TlvItem<'a, CacheInfo>> { + ::openprot_tlv::TlvQuery::ext(self) + .iter::() + .next() + } + #[inline(always)] + fn execution_units(&self) -> ::openprot_tlv::TlvIterator<'a, ExecutionUnit> { + ::openprot_tlv::TlvQuery::ext(self).iter::() + } +} + +#[bitfield(u8)] +#[derive(PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +pub struct BusFeatures { + #[bits(1)] + pub is_primary: bool, + #[bits(1)] + pub supports_dma: bool, + #[bits(1)] + pub supports_coherent: bool, + #[bits(5)] + pub burst_len_log2: u8, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct SystemBus { + pub bus_type: u32, + pub width_bits: u32, + pub speed_mhz: u32, + pub features: BusFeatures, + pub __pad0: [u8; 3usize], +} +const _: () = { + assert!(::core::mem::offset_of!(SystemBus, bus_type) == 0usize); + assert!(::core::mem::offset_of!(SystemBus, width_bits) == 4usize); + assert!(::core::mem::offset_of!(SystemBus, speed_mhz) == 8usize); + assert!(::core::mem::offset_of!(SystemBus, features) == 12usize); + assert!(::core::mem::align_of::() <= 4); + assert!(::core::mem::size_of::() == 16usize); +}; +impl ::core::default::Default for SystemBus { + fn default() -> Self { + ::zerocopy::FromZeros::new_zeroed() + } +} +impl ::openprot_tlv::TlvObject for SystemBus { + type Extension = [u8]; + const TAG: u32 = u32::from_le_bytes(*b"SBUS"); + fn make_ext<'a>(header: &::openprot_tlv::TlvHeader, extra: &'a [u32]) -> &'a Self::Extension { + ::openprot_tlv::unpad(header, extra) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct CacheInfo { + pub level: u32, + pub size_kib: u32, + pub associativity: u32, +} +const _: () = { + assert!(::core::mem::offset_of!(CacheInfo, level) == 0usize); + assert!(::core::mem::offset_of!(CacheInfo, size_kib) == 4usize); + assert!(::core::mem::offset_of!(CacheInfo, associativity) == 8usize); + assert!(::core::mem::align_of::() <= 4); + assert!(::core::mem::size_of::() == 12usize); +}; +impl ::core::default::Default for CacheInfo { + fn default() -> Self { + ::zerocopy::FromZeros::new_zeroed() + } +} +impl ::openprot_tlv::TlvObject for CacheInfo { + type Extension = [u8]; + const TAG: u32 = u32::from_le_bytes(*b"CACH"); + fn make_ext<'a>(header: &::openprot_tlv::TlvHeader, extra: &'a [u32]) -> &'a Self::Extension { + ::openprot_tlv::unpad(header, extra) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct ExecutionUnit { + pub unit_type: u8, + pub pipeline_stages: u8, + pub state: ExecutionUnitState, + pub __pad0: [u8; 1usize], + pub power_mw: u16, + pub __pad1: [u8; 2usize], +} +const _: () = { + assert!(::core::mem::offset_of!(ExecutionUnit, unit_type) == 0usize); + assert!(::core::mem::offset_of!(ExecutionUnit, pipeline_stages) == 1usize); + assert!(::core::mem::offset_of!(ExecutionUnit, state) == 2usize); + assert!(::core::mem::offset_of!(ExecutionUnit, power_mw) == 4usize); + assert!(::core::mem::align_of::() <= 4); + assert!(::core::mem::size_of::() == 8usize); +}; +impl ::core::default::Default for ExecutionUnit { + fn default() -> Self { + ::zerocopy::FromZeros::new_zeroed() + } +} +impl ::openprot_tlv::TlvObject for ExecutionUnit { + type Extension = [u8]; + const TAG: u32 = u32::from_le_bytes(*b"EXEU"); + fn make_ext<'a>(header: &::openprot_tlv::TlvHeader, extra: &'a [u32]) -> &'a Self::Extension { + ::openprot_tlv::unpad(header, extra) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct GpuAccelerator { + pub cores: u32, + pub frequency_mhz: u32, +} +const _: () = { + assert!(::core::mem::offset_of!(GpuAccelerator, cores) == 0usize); + assert!(::core::mem::offset_of!(GpuAccelerator, frequency_mhz) == 4usize); + assert!(::core::mem::align_of::() <= 4); + assert!(::core::mem::size_of::() == 8usize); +}; +impl ::core::default::Default for GpuAccelerator { + fn default() -> Self { + ::zerocopy::FromZeros::new_zeroed() + } +} +impl ::openprot_tlv::TlvObject for GpuAccelerator { + type Extension = [u8]; + const TAG: u32 = u32::from_le_bytes(*b"GPU_"); + fn make_ext<'a>(header: &::openprot_tlv::TlvHeader, extra: &'a [u32]) -> &'a Self::Extension { + ::openprot_tlv::unpad(header, extra) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct NpuAccelerator { + pub tops: u32, +} +const _: () = { + assert!(::core::mem::offset_of!(NpuAccelerator, tops) == 0usize); + assert!(::core::mem::align_of::() <= 4); + assert!(::core::mem::size_of::() == 4usize); +}; +impl ::core::default::Default for NpuAccelerator { + fn default() -> Self { + ::zerocopy::FromZeros::new_zeroed() + } +} +impl ::openprot_tlv::TlvObject for NpuAccelerator { + type Extension = [u8]; + const TAG: u32 = u32::from_le_bytes(*b"NPU_"); + fn make_ext<'a>(header: &::openprot_tlv::TlvHeader, extra: &'a [u32]) -> &'a Self::Extension { + ::openprot_tlv::unpad(header, extra) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct CryptoAccelerator { + pub key_size_bits: u32, +} +const _: () = { + assert!(::core::mem::offset_of!(CryptoAccelerator, key_size_bits) == 0usize); + assert!(::core::mem::align_of::() <= 4); + assert!(::core::mem::size_of::() == 4usize); +}; +impl ::core::default::Default for CryptoAccelerator { + fn default() -> Self { + ::zerocopy::FromZeros::new_zeroed() + } +} +impl ::openprot_tlv::TlvObject for CryptoAccelerator { + type Extension = [u8]; + const TAG: u32 = u32::from_le_bytes(*b"CRYP"); + fn make_ext<'a>(header: &::openprot_tlv::TlvHeader, extra: &'a [u32]) -> &'a Self::Extension { + ::openprot_tlv::unpad(header, extra) + } +} +pub trait CryptoAcceleratorExt<'a> { + fn supported_ciphers(&self) -> &'a [u8]; +} +impl<'a> CryptoAcceleratorExt<'a> for ::openprot_tlv::TlvItem<'a, CryptoAccelerator> { + #[inline(always)] + fn supported_ciphers(&self) -> &'a [u8] { + use ::openprot_tlv::TlvQuery; + self.ext() + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct Accelerator {} +const _: () = { + assert!(::core::mem::align_of::() <= 4); + assert!(::core::mem::size_of::() == 0usize); +}; +impl ::core::default::Default for Accelerator { + fn default() -> Self { + ::zerocopy::FromZeros::new_zeroed() + } +} +impl ::openprot_tlv::TlvObject for Accelerator { + type Extension = ::openprot_tlv::TlvData; + const TAG: u32 = u32::from_le_bytes(*b"ACCL"); + fn make_ext<'a>(_header: &::openprot_tlv::TlvHeader, extra: &'a [u32]) -> &'a Self::Extension { + ::openprot_tlv::TlvData::overlay(extra) + } +} +pub enum AcceleratorConfig<'a> { + Gpu(::openprot_tlv::TlvItem<'a, GpuAccelerator>), + Npu(::openprot_tlv::TlvItem<'a, NpuAccelerator>), + Crypto(::openprot_tlv::TlvItem<'a, CryptoAccelerator>), +} +pub trait AcceleratorExt<'a> { + fn gpu(&self) -> Option<::openprot_tlv::TlvItem<'a, GpuAccelerator>>; + fn npu(&self) -> Option<::openprot_tlv::TlvItem<'a, NpuAccelerator>>; + fn crypto(&self) -> Option<::openprot_tlv::TlvItem<'a, CryptoAccelerator>>; + fn config(&self) -> Option>; +} +impl<'a> AcceleratorExt<'a> for ::openprot_tlv::TlvItem<'a, Accelerator> { + #[inline(always)] + fn gpu(&self) -> Option<::openprot_tlv::TlvItem<'a, GpuAccelerator>> { + ::openprot_tlv::TlvQuery::ext(self) + .iter::() + .next() + } + #[inline(always)] + fn npu(&self) -> Option<::openprot_tlv::TlvItem<'a, NpuAccelerator>> { + ::openprot_tlv::TlvQuery::ext(self) + .iter::() + .next() + } + #[inline(always)] + fn crypto(&self) -> Option<::openprot_tlv::TlvItem<'a, CryptoAccelerator>> { + ::openprot_tlv::TlvQuery::ext(self) + .iter::() + .next() + } + #[inline(always)] + fn config(&self) -> Option> { + use ::openprot_tlv::TlvQuery; + for item in ::openprot_tlv::TlvQuery::ext(self).iter::<::openprot_tlv::TlvAny>() { + if let Some(val) = item.cast::() { + return Some(AcceleratorConfig::Gpu(val)); + } + if let Some(val) = item.cast::() { + return Some(AcceleratorConfig::Npu(val)); + } + if let Some(val) = item.cast::() { + return Some(AcceleratorConfig::Crypto(val)); + } + } + None + } +}