From: Jiri Pirko <jiri@resnulli.us>
To: linux-coco@lists.linux.dev
Cc: linux-kernel@vger.kernel.org, linux-api@vger.kernel.org,
x86@kernel.org, kvm@vger.kernel.org, linux-doc@vger.kernel.org,
jgg@ziepe.ca, david.quigley@intel.com, lukas@wunner.de,
ksinyuk@kernel.org, rodrigo.vivi@intel.com,
cedric.xing@intel.com, sameo@meta.com, drdeeglaze@gmail.com,
James.Bottomley@HansenPartnership.com, aik@amd.com,
jpiotrowski@nvidia.com, qinkun@google.com, pgonda@google.com,
jic23@kernel.org, alistair.francis@wdc.com, ankita@nvidia.com,
suzuki.poulose@arm.com, kas@kernel.org,
rick.p.edgecombe@intel.com, elena.reshetova@intel.com,
dave.hansen@linux.intel.com,
sathyanarayanan.kuppuswamy@linux.intel.com, peter.fang@intel.com,
jarkko@kernel.org, zohar@linux.ibm.com, yeoreum.yun@arm.com,
aneesh.kumar@kernel.org, dave.jiang@intel.com,
nik.borisov@suse.com, sergii.dmytruk@3mdeb.com
Subject: [PATCH RFC v2 15/15] tools/ctlv: Add ctlvctl
Date: Fri, 13 Nov 2026 16:55:14 +0100 [thread overview]
Message-ID: <20261113155515.2510194-16-jiri@resnulli.us> (raw)
In-Reply-To: <20261113155515.2510194-1-jiri@resnulli.us>
From: Jiri Pirko <jiri@nvidia.com>
Add a command-line client that discovers and queries any CTLV device,
with optional names from a specification directory.
Examples:
Root-family discovery on any CTLV device:
ctlvctl --spec-dir Documentation/ctlv/specs \
exec ctlv-example family-get
The operations the same device publishes:
ctlvctl --spec-dir Documentation/ctlv/specs ops ctlv-example
Assisted-by: Cursor: GPT-5.6 Sol
Assisted-by: Cursor: Claude Opus 5.5
Signed-off-by: Jiri Pirko <jiri@nvidia.com>
---
Documentation/driver-api/ctlv.rst | 29 +-
Documentation/userspace-api/ctlv.rst | 80 +-
tools/Makefile | 9 +-
tools/ctlv/Makefile | 23 +-
tools/ctlv/ctlvctl/.gitignore | 4 +
tools/ctlv/ctlvctl/Makefile | 64 +
tools/ctlv/ctlvctl/ctlvctl.c | 2787 +++++++++++++++++++++++++
tools/ctlv/ctlvctl/spec.c | 672 ++++++
tools/ctlv/ctlvctl/spec.h | 60 +
tools/ctlv/ctlvgen/cli.py | 47 +-
tools/ctlv/ctlvgen/lib/dump.py | 161 ++
tools/ctlv/ctlvgen/lib/gen_doc.py | 684 ++++++
tools/ctlv/ctlvgen/lib/gen_runtime.py | 66 +
13 files changed, 4661 insertions(+), 25 deletions(-)
create mode 100644 tools/ctlv/ctlvctl/.gitignore
create mode 100644 tools/ctlv/ctlvctl/Makefile
create mode 100644 tools/ctlv/ctlvctl/ctlvctl.c
create mode 100644 tools/ctlv/ctlvctl/spec.c
create mode 100644 tools/ctlv/ctlvctl/spec.h
create mode 100644 tools/ctlv/ctlvgen/lib/dump.py
create mode 100644 tools/ctlv/ctlvgen/lib/gen_doc.py
create mode 100644 tools/ctlv/ctlvgen/lib/gen_runtime.py
diff --git a/Documentation/driver-api/ctlv.rst b/Documentation/driver-api/ctlv.rst
index f6a4f9632e3a..e8e6ecdf6ab6 100644
--- a/Documentation/driver-api/ctlv.rst
+++ b/Documentation/driver-api/ctlv.rst
@@ -235,6 +235,9 @@ checks the same without writing anything.
Never edit a generated file; change the YAML or the generator and regenerate.
+``make -C tools/ctlv docs`` writes optional RST reference pages to
+``Documentation/output/ctlv/families``.
+
Create and destroy a device
===========================
@@ -684,20 +687,24 @@ The node is of the foo driver's own class, ``foo_class``, registered as
that ``ctlv_family_inherits()`` says is not ``ctlv-example`` or a descendant,
then calls ``ctlv_init()``.
-Reporting an error
-------------------
+Tests and examples
+==================
+
+``Documentation/ctlv/specs/ctlv-test.yaml``
+ Every schema kind, records, actions, queries, blobs, availability, hooks,
+ and an extension point.
+
+``drivers/ctlv/tests/ctlv_test.c``
+ The ``ctlv-test`` family implementation.
-A callback returns a negative errno, and that is what the ioctl returns. Where
-CTLV itself refuses a message it also writes error information into the
-caller's reply buffer, naming the reason and the offset of the offending
-TLV. A family implementation does not build that; it adds its own errno
-and its reply.
+``drivers/ctlv/tests/ctlv_test_child.c``
+ A separately built inheriting family and extension-point fill.
-Configuration
-=============
+``lib/ctlv/tests/*_kunit.c``
+ Unit tests of the engine and of its nodes.
-``CONFIG_CTLV``
- CTLV. A family implementation selects it.
+``tools/testing/selftests/ctlv/``
+ Raw-message, generated-binding, allocation-failure, and mutation selftests.
Kernel API
==========
diff --git a/Documentation/userspace-api/ctlv.rst b/Documentation/userspace-api/ctlv.rst
index 8a8ade33b490..dc590f334545 100644
--- a/Documentation/userspace-api/ctlv.rst
+++ b/Documentation/userspace-api/ctlv.rst
@@ -58,8 +58,8 @@ Each part of the design serves one of the needs:
so that a caller whose answer changed between the parts gets ``ESTALE``
rather than a mix of two states.
- Introspection. Every device answers ``family-get``, ``op-get`` and
- ``op-schema-get``, so a program built without a family still learns what
- the device accepts.
+ ``op-schema-get``, so a program built without a family, such as ``ctlvctl``,
+ still learns what the device accepts.
`Compatibility`_ collects the rules for growing a family.
@@ -133,6 +133,11 @@ specification: ``index`` must be at most 4095 and ``label`` at most 63 bytes.
ctlv_example_item_set_label_put(req, "ab");
ctlv_example_item_set(dev, req, &error);
+and so does ``ctlvctl``, given the family's specification::
+
+ ctlvctl --spec-dir Documentation/ctlv/specs \
+ exec /dev/ctlv-example item-set index=3 label=ab
+
The sections below define each piece: the device, the message format, the
ioctl, queries, blobs and introspection, and then the rules of compatibility.
@@ -564,7 +569,7 @@ cut. The first call passes ``generation`` and ``cursor`` zero. On return:
To continue, the caller repeats the call with the same ``op_id`` and request,
the returned ``cursor`` and the returned ``generation``, and reassembles by
-concatenating the children of each still-open record.
+concatenating the children of each still-open record, which ``libctlv`` does.
One answer of three items, cut by a buffer that holds two::
@@ -591,7 +596,8 @@ it, so a reader knows whether to append or to open a new record beside it.
Every continuing fragment adds at least one complete logical TLV beyond its
repeated path, and a cursor value cannot recur within one logical reply.
-A reader must reject either violation.
+``libctlv`` rejects either violation. It also limits one query to 65,536
+fragments and a 64 MiB reassembled reply.
A continuation whose generation no longer matches fails with ``-ESTALE``, and
the error information carries the expected and the current generation.
@@ -709,3 +715,69 @@ removing or renumbering anything, changing a field's kind or an operation's
type, access or availability, narrowing a bound or an occurrence limit,
making a field that the kernel writes optional, and, in any message,
making a field required or adding a required one.
+
+Library and tools
+=================
+
+``tools/ctlv/libctlv`` is a small C library for the mechanical parts: opening
+a device, checking the chain, building requests, parsing replies, reassembling
+fragmented queries and building blob descriptors. Per-family
+bindings are generated beside it, turning operations into typed functions.
+``ctlv_open()`` finds a device by the family its uevent announces, whichever
+subsystem it is in. ``ctlv_open_fd()`` takes a file the caller opened, through
+another subsystem's interface if need be, and ``ctlv_close()`` leaves it open.
+
+``tools/ctlv/ctlvctl`` defaults to runtime introspection alone. Compiled
+against no family, it can list devices, print the operations one publishes and
+their schemas, and execute its actions and queries alike. A device is
+named as ``list`` prints it, relative to ``/dev``, or by a path::
+
+ ctlvctl list
+ ctlvctl ops /dev/ctlv-example
+ ctlvctl schema /dev/ctlv-example 0x0001000000000008
+ ctlvctl exec /dev/ctlv-example 0x0001000000000001
+ ctlvctl exec /dev/ctlv-example 0x0001000000000002 0x0001800200000001=7
+
+Passing the directory of the kernel's family specifications adds the names
+that are deliberately absent from introspection. This is the ``specs``
+directory of a CTLV documentation bundle; its registry and meta-schemas are in
+the parent directory::
+
+ ctlvctl --spec-dir Documentation/ctlv/specs ops /dev/ctlv-example
+ ctlvctl --spec-dir Documentation/ctlv/specs \
+ schema /dev/ctlv-example series-get
+ ctlvctl --spec-dir Documentation/ctlv/specs \
+ exec /dev/ctlv-example probe-get token=7
+ ctlvctl --spec-dir Documentation/ctlv/specs \
+ exec /dev/ctlv-example echo-reply \
+ --json '{"u":17,"mode":"busy","count":[3,5]}'
+ ctlvctl --spec-dir Documentation/ctlv/specs \
+ exec /dev/ctlv-example transfer --json \
+ '{"source":{"file":"in"},"target":{"file":"out","length":64}}'
+
+``CTLV_SPEC_DIR`` avoids repeating the directory. Known items are printed by
+name without IDs; ``-n`` adds them, while an item unknown to the selected
+specifications stays numeric. ``schema --json-template`` prints a minimal JSON
+request. ``--output-json`` emits one object for an action or query. A JSON
+request may be inline, read from ``@file``, or read from standard input with
+``--json -``; it cannot be combined with assignments. Specification and JSON
+support are built when json-c is available.
+
+In JSON, a blob is ``{"file": "<path>"}``, passed as ``va`` memory. An input
+blob holds the file. An output blob is written to the file whole, so give it
+the ``"length"`` the operation writes; without one it starts at a byte and
+grows to each ``required-length`` the operation fails with, the operation
+being retried. An ``inout`` blob is read from the file and written back.
+
+In the directory, ``ctlvctl`` reads a family's names from ``<family>.json``
+when it is there, and otherwise has ``ctlvgen``, with its Python dependencies,
+resolve ``<family>.yaml``. Building ``ctlvctl`` writes the ``.json`` of each of
+the tree's families beside it, so a machine with neither the source tree nor
+Python needs only that directory and json-c. ``ctlvgen --spec <family>.yaml
+--runtime-json`` prints the file for a family outside the tree.
+
+``make -C tools/ctlv programs`` builds both in the source tree. After ``make
+O=<dir> headers``, ``make O=<dir> tools/ctlv`` builds ``ctlvctl`` into
+``<dir>/tools/ctlv/ctlvctl`` against that tree's headers, for another
+architecture with ``CROSS_COMPILE`` or ``LLVM`` as the kernel takes them;
+``PKG_CONFIG`` names the ``pkg-config`` that finds the target's json-c.
diff --git a/tools/Makefile b/tools/Makefile
index cb40961a740f..d8d0003b16a5 100644
--- a/tools/Makefile
+++ b/tools/Makefile
@@ -13,6 +13,7 @@ help:
@echo ' bpf - misc BPF tools'
@echo ' counter - counter tools'
@echo ' cpupower - a tool for all things x86 CPU power'
+ @echo ' ctlv - CTLV library and ctlvctl, after make headers'
@echo ' debugging - tools for debugging'
@echo ' dma - tools for DMA mapping'
@echo ' firewire - the userspace part of nosy, an IEEE-1394 traffic sniffer'
@@ -70,6 +71,9 @@ acpi: FORCE
cpupower: FORCE
$(call descend,power/$@)
+ctlv: FORCE
+ $(call descend,ctlv/ctlvctl)
+
counter dma firewire hv guest bootconfig spi usb virtio mm bpf iio gpio objtool leds wmi firmware debugging tracing: FORCE
$(call descend,$@)
@@ -179,6 +183,9 @@ acpi_clean:
cpupower_clean:
$(call descend,power/cpupower,clean)
+ctlv_clean:
+ $(call descend,ctlv/ctlvctl,clean)
+
counter_clean dma_clean hv_clean firewire_clean bootconfig_clean spi_clean usb_clean virtio_clean mm_clean wmi_clean bpf_clean iio_clean gpio_clean objtool_clean leds_clean firmware_clean debugging_clean tracing_clean:
$(call descend,$(@:_clean=),clean)
@@ -225,7 +232,7 @@ build_clean:
ynl_clean:
$(call descend,net/$(@:_clean=),clean)
-clean: acpi_clean counter_clean cpupower_clean dma_clean hv_clean firewire_clean \
+clean: acpi_clean counter_clean cpupower_clean ctlv_clean dma_clean hv_clean firewire_clean \
perf_clean selftests_clean turbostat_clean bootconfig_clean spi_clean usb_clean virtio_clean \
mm_clean bpf_clean iio_clean x86_energy_perf_policy_clean tmon_clean \
freefall_clean build_clean libbpf_clean libsubcmd_clean \
diff --git a/tools/ctlv/Makefile b/tools/ctlv/Makefile
index 881fae9127c7..83c0ece690e8 100644
--- a/tools/ctlv/Makefile
+++ b/tools/ctlv/Makefile
@@ -2,6 +2,10 @@
KDIR = ../..
DOCDIR = $(KDIR)/Documentation/ctlv
+SPECDIR = $(DOCDIR)/specs
+SPECS = $(wildcard $(SPECDIR)/*.yaml)
+TOOL = ./ctlvgen/cli.py
+DOC_OUTPUT ?= $(KDIR)/Documentation/output/ctlv/families
# Writes nothing. Fails on an invalid specification, and on any generated file
# or ABI snapshot that regen would change.
@@ -30,16 +34,29 @@ lint:
python3 -m pyflakes $(PYSRC)
python3 -m mypy $(PYSRC)
-# The C half of this directory: the userspace library, which is not generated
-# and not Python, so it is its own target rather than part of the checks above.
+docs:
+ @mkdir -p $(DOC_OUTPUT)
+ @for spec in $(SPECS) ; do \
+ name=$$(basename $$spec .yaml) ; \
+ echo -e "\tDOC $$name.rst" ; \
+ $(TOOL) --spec $$spec --mode doc --rst \
+ -o $(DOC_OUTPUT)/$$name.rst || exit 1 ; \
+ done
+
+# The C half of this directory: the userspace library, and the tool that drives
+# a device it was never compiled against. Neither is generated and neither is
+# Python, so they are their own target rather than part of the checks above.
programs:
$(MAKE) -C libctlv
+ $(MAKE) -C ctlvctl
clean:
rm -rf ctlvgen/__pycache__
rm -rf ctlvgen/lib/__pycache__
rm -rf ctlvgen.egg-info
rm -rf build
+ rm -rf $(DOC_OUTPUT)
$(MAKE) -C libctlv clean
+ $(MAKE) -C ctlvctl clean
-.PHONY: all regen lint programs clean
+.PHONY: all regen lint docs programs clean
diff --git a/tools/ctlv/ctlvctl/.gitignore b/tools/ctlv/ctlvctl/.gitignore
new file mode 100644
index 000000000000..dcade74af00e
--- /dev/null
+++ b/tools/ctlv/ctlvctl/.gitignore
@@ -0,0 +1,4 @@
+# SPDX-License-Identifier: GPL-2.0-only
+/*.json
+/ctlvctl
+/libctlv/
diff --git a/tools/ctlv/ctlvctl/Makefile b/tools/ctlv/ctlvctl/Makefile
new file mode 100644
index 000000000000..dcfca390d402
--- /dev/null
+++ b/tools/ctlv/ctlvctl/Makefile
@@ -0,0 +1,64 @@
+# SPDX-License-Identifier: GPL-2.0
+
+KDIR ?= ../../..
+LIBDIR = ../libctlv
+
+CFLAGS ?= -O2 -g
+
+include ../../scripts/Makefile.include
+
+KHDR_INCLUDES ?= -isystem $(or $(ABSOLUTE_O),$(KDIR))/usr/include
+
+CFLAGS += -Wall -Wextra -Wno-unused-parameter -Wno-missing-field-initializers
+CFLAGS += -D_GNU_SOURCE -I$(LIBDIR) $(KHDR_INCLUDES)
+
+# The one that knows the target's json-c, when cross-compiling.
+PKG_CONFIG ?= $(CROSS_COMPILE)pkg-config
+
+JSON_C_CFLAGS := $(shell $(PKG_CONFIG) --atleast-version=0.14 json-c \
+ 2>/dev/null && $(PKG_CONFIG) --cflags json-c)
+JSON_C_LIBS := $(shell $(PKG_CONFIG) --atleast-version=0.14 json-c \
+ 2>/dev/null && $(PKG_CONFIG) --libs json-c)
+ifneq ($(JSON_C_LIBS),)
+CFLAGS += -DHAVE_JSON_C $(JSON_C_CFLAGS)
+LDLIBS += $(JSON_C_LIBS)
+endif
+
+# With a build directory, libctlv is built for ctlvctl inside it.
+LIBCTLV_OUTPUT ?= $(if $(OUTPUT),$(OUTPUT)libctlv/)
+LIB = $(or $(LIBCTLV_OUTPUT),$(LIBDIR)/)libctlv.a
+OBJS = $(OUTPUT)ctlvctl.o $(OUTPUT)spec.o
+
+# What --spec-dir reads where there is neither the source tree nor Python:
+# the names of each of the tree's families and of the families it inherits.
+DOCDIR = $(KDIR)/Documentation/ctlv
+SPECDIR = $(DOCDIR)/specs
+SPECS = $(wildcard $(SPECDIR)/*.yaml)
+CTLVGEN = ../ctlvgen/cli.py
+CTLVGEN_INPUTS = $(SPECS) $(wildcard $(DOCDIR)/*.yaml) \
+ $(wildcard ../ctlvgen/*.py ../ctlvgen/lib/*.py)
+MANIFESTS = $(patsubst $(SPECDIR)/%.yaml,$(OUTPUT)%.json,$(SPECS))
+
+all: $(OUTPUT)ctlvctl $(MANIFESTS)
+
+$(OUTPUT)ctlvctl: $(OBJS) $(LIB)
+ $(QUIET_LINK)$(CC) $(CFLAGS) -o $@ $(OBJS) $(LIB) $(LDLIBS)
+
+$(OUTPUT)%.o: %.c spec.h
+ $(QUIET_CC)$(CC) $(CFLAGS) -c -o $@ $<
+
+$(OUTPUT)%.json: $(SPECDIR)/%.yaml $(CTLVGEN_INPUTS)
+ $(QUIET_GEN)$(CTLVGEN) --spec $< --runtime-json > $@
+
+$(LIB): FORCE
+ $(if $(LIBCTLV_OUTPUT),$(Q)mkdir -p $(LIBCTLV_OUTPUT))
+ $(Q)$(MAKE) -C $(LIBDIR) OUTPUT=$(LIBCTLV_OUTPUT)
+
+clean:
+ rm -f $(OUTPUT)ctlvctl $(OBJS) $(MANIFESTS)
+ $(if $(LIBCTLV_OUTPUT),rm -rf $(LIBCTLV_OUTPUT))
+
+FORCE:
+
+.PHONY: all clean FORCE
+.DELETE_ON_ERROR:
diff --git a/tools/ctlv/ctlvctl/ctlvctl.c b/tools/ctlv/ctlvctl/ctlvctl.c
new file mode 100644
index 000000000000..ef9bb8536c47
--- /dev/null
+++ b/tools/ctlv/ctlvctl/ctlvctl.c
@@ -0,0 +1,2787 @@
+// SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
+/*
+ * Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES
+ *
+ * Family-independent CTLV command-line client.
+ */
+
+#include <ctype.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <getopt.h>
+#include <limits.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#ifdef HAVE_JSON_C
+#include <json.h>
+#endif
+
+#include <ctlv.h>
+
+#include "spec.h"
+
+#define DEV_DIR "/dev"
+#define PATH_LEN 1024
+#define LINE_LEN 512
+#define ARRAY_SIZE(array) (sizeof(array) / sizeof((array)[0]))
+
+/*
+ * How deep a request built from the command line may go: the root and 32
+ * levels below it, as deep as any device goes today.
+ */
+#define PATH_DEPTH 33
+
+/* Where a request buffer starts; it grows as fields are added. */
+#define REQUEST_HINT 512
+
+static const char *prog = "ctlvctl";
+
+static struct {
+ const char *spec_dir;
+ bool show_ids;
+ bool output_json;
+} options;
+
+#ifdef HAVE_JSON_C
+static int json_template_print(const struct ctlv_node_schema *root,
+ const struct ctlvctl_spec *spec);
+static void json_add_id(struct json_object *object, const char *key, __u64 id);
+static void json_add_op(struct json_object *object,
+ const struct ctlvctl_spec *spec, __u64 op_id);
+static int json_print(struct json_object *object);
+static int json_schema_print(const struct ctlv_dev *dev,
+ const struct ctlvctl_spec *spec, __u64 op_id,
+ const struct ctlv_schema *schema);
+
+static void json_add_string(struct json_object *object, const char *key,
+ const char *value)
+{
+ json_object_object_add(object, key, json_object_new_string(value));
+}
+
+static void json_add_u64(struct json_object *object, const char *key,
+ __u64 value)
+{
+ json_object_object_add(object, key, json_object_new_uint64(value));
+}
+
+static void json_add_s64(struct json_object *object, const char *key,
+ __s64 value)
+{
+ json_object_object_add(object, key, json_object_new_int64(value));
+}
+
+static void json_add_int(struct json_object *object, const char *key, int value)
+{
+ json_object_object_add(object, key, json_object_new_int(value));
+}
+
+static void json_add_bool(struct json_object *object, const char *key,
+ bool value)
+{
+ json_object_object_add(object, key, json_object_new_boolean(value));
+}
+
+static int json_array_add_string(struct json_object *array, const char *value)
+{
+ struct json_object *string = json_object_new_string(value);
+
+ if (!string)
+ return -ENOMEM;
+ if (json_object_array_add(array, string)) {
+ json_object_put(string);
+ return -ENOMEM;
+ }
+ return 0;
+}
+#endif
+
+static void help(FILE *out)
+{
+ fprintf(out,
+ "usage: %s [options] <command> [arguments]\n"
+ "\n"
+ " list every CTLV device and its family chain\n"
+ " ops <device> every op the device publishes\n"
+ " schema [--json-template] <device> <op>\n"
+ " the request and reply ABI of one op\n"
+ " exec [-l len] <device> <op> [a=v]\n"
+ " run an op, reassembling a query's answer\n"
+ "\n"
+ " --spec-dir DIR use DIR/<family>.json or .yaml\n"
+ " -n, --show-ids show IDs beside specification names\n"
+ " --output-json print structured JSON\n"
+ " --json TEXT|@FILE|- build a request from JSON\n"
+ "\n"
+ "In --json, a blob is {\"file\": PATH}. Input is read from the file, and\n"
+ "output written to it: \"length\" bytes, or as many as the op needs.\n"
+ "\n"
+ "-l names the reply buffer, which for a query decides how many fragments\n"
+ "the answer arrives in; the device reports its smallest usable size in ops.\n"
+ "\n"
+ "With --spec-dir, operation names and top-level scalar names may be\n"
+ "used. Otherwise an operation goes by its ID and a field by its type.\n"
+ "A dotted numeric path enters a record:\n"
+ "\n"
+ " %s exec ctlv-test0 0x3000000000009 0x3800900000001=7\n"
+ " %s exec ctlv-test0 0x3000000000006 0x3000100000001=1 \\\n"
+ " 0x3000100000009.0x3000110000001=3\n",
+ prog, prog, prog);
+}
+
+static int parse_u64(const char *text, __u64 *value)
+{
+ char *end;
+
+ if (!*text || *text == '-' || isspace((unsigned char)*text))
+ return -EINVAL;
+ errno = 0;
+ *value = strtoull(text, &end, 0);
+ if (errno || end == text || *end)
+ return -EINVAL;
+ return 0;
+}
+
+static int parse_s64(const char *text, __s64 *value)
+{
+ char *end;
+
+ errno = 0;
+ *value = strtoll(text, &end, 0);
+ if (errno || end == text || *end)
+ return -EINVAL;
+ return 0;
+}
+
+static int dev_open(const char *name, int flags, struct ctlv_dev **devp)
+{
+ char path[PATH_LEN];
+ int ret;
+
+ /* A listed name is under /dev even when it has a directory of its own. */
+ if (name[0] == '/' || name[0] == '.')
+ snprintf(path, sizeof(path), "%s", name);
+ else
+ snprintf(path, sizeof(path), "%s/%s", DEV_DIR, name);
+ ret = ctlv_open_path(path, flags, devp);
+ if (ret)
+ fprintf(stderr, "%s: %s: %s\n", prog, path, strerror(-ret));
+ return ret;
+}
+
+static int spec_load(const struct ctlv_dev *dev, struct ctlvctl_spec **spec)
+{
+ if (!options.spec_dir) {
+ *spec = NULL;
+ return 0;
+ }
+ return ctlvctl_spec_load(prog, options.spec_dir, dev, spec);
+}
+
+static int op_resolve(const struct ctlvctl_spec *spec, const char *text,
+ __u64 *op_id)
+{
+ const struct ctlvctl_op_name *op;
+
+ if (!parse_u64(text, op_id))
+ return 0;
+ op = ctlvctl_spec_op_name(spec, text);
+ if (op) {
+ *op_id = op->id;
+ return 0;
+ }
+ if (spec)
+ fprintf(stderr, "%s: no operation \"%s\"\n", prog, text);
+ else
+ fprintf(stderr, "%s: %s: not an op ID\n", prog, text);
+ return -ENOENT;
+}
+
+static void chain_print(const struct ctlv_dev *dev)
+{
+ unsigned int i;
+
+ for (i = 0; i < ctlv_chain_len(dev); i++)
+ printf("%s%s", i ? "/" : "", ctlv_chain_name(dev, i));
+}
+
+static int cmd_list(int argc, char **argv)
+{
+#ifdef HAVE_JSON_C
+ struct json_object *json = NULL;
+#endif
+ struct ctlv_dev_list list;
+ unsigned int i;
+ int ret;
+
+ if (argc) {
+ help(stderr);
+ return -EINVAL;
+ }
+ ret = ctlv_enumerate(&list);
+ if (ret) {
+ fprintf(stderr, "%s: no CTLV devices: %s\n", prog,
+ strerror(-ret));
+ return ret;
+ }
+#ifdef HAVE_JSON_C
+ if (options.output_json) {
+ json = json_object_new_array();
+ if (!json) {
+ ctlv_dev_list_free(&list);
+ return -ENOMEM;
+ }
+ }
+#endif
+ for (i = 0; i < list.names_count; i++) {
+ struct ctlv_dev *dev;
+
+#ifdef HAVE_JSON_C
+ if (json) {
+ struct json_object *chain = json_object_new_array();
+ struct json_object *item = json_object_new_object();
+ unsigned int level;
+
+ json_add_string(item, "device", list.names[i]);
+ if (!dev_open(list.names[i], O_RDONLY, &dev)) {
+ for (level = 0; level < ctlv_chain_len(dev);
+ level++) {
+ struct json_object *family =
+ json_object_new_object();
+
+ json_add_string(family, "name",
+ ctlv_chain_name(dev,
+ level));
+ if (options.show_ids)
+ json_add_int(family, "id",
+ ctlv_chain_id(dev, level));
+ json_object_array_add(chain, family);
+ }
+ ctlv_close(dev);
+ }
+ json_object_object_add(item, "chain", chain);
+ json_object_array_add(json, item);
+ continue;
+ }
+#endif
+ printf("%s", list.names[i]);
+ /* Listing should not need the right to change a device. */
+ if (!dev_open(list.names[i], O_RDONLY, &dev)) {
+ printf("\t");
+ chain_print(dev);
+ ctlv_close(dev);
+ }
+ printf("\n");
+ }
+ ctlv_dev_list_free(&list);
+#ifdef HAVE_JSON_C
+ if (json)
+ return json_print(json);
+#endif
+ return 0;
+}
+
+static const char *type_name(__u32 type)
+{
+ switch (type) {
+ case CTLV_OP_TYPE_ACTION:
+ return "action";
+ case CTLV_OP_TYPE_QUERY:
+ return "query";
+ default:
+ return "?";
+ }
+}
+
+static const char *fd_access_name(__u32 fd_access)
+{
+ return fd_access == CTLV_OP_FD_ACCESS_READ ? "read" : "read-write";
+}
+
+static bool op_matches(const struct ctlvctl_op_name *name,
+ const struct ctlv_op_info *op)
+{
+ return name->type == op->type &&
+ name->fd_access == op->fd_access &&
+ name->availability == op->availability;
+}
+
+static int op_print(const struct ctlvctl_spec *spec,
+ const struct ctlv_op_info *op)
+{
+ const struct ctlvctl_op_name *name;
+
+ name = ctlvctl_spec_op_id(spec, op->op_id);
+ if (name && !op_matches(name, op)) {
+ fprintf(stderr,
+ "%s: specification disagrees with operation %#llx\n",
+ prog, (unsigned long long)op->op_id);
+ return -ENOMSG;
+ }
+ if (name) {
+ printf("%-20s", name->name);
+ if (options.show_ids)
+ printf("\t%#018llx",
+ (unsigned long long)op->op_id);
+ } else {
+ printf("%#018llx", (unsigned long long)op->op_id);
+ }
+ printf("\t%-6s", type_name(op->type));
+ if (op->fd_access)
+ printf("\t%s", fd_access_name(op->fd_access));
+ if (op->availability == CTLV_OP_AVAILABILITY_DYNAMIC)
+ printf("\tdynamic %s", op->disabled ? "disabled" : "enabled");
+ else if (op->availability == CTLV_OP_AVAILABILITY_FIXED)
+ printf("\tfixed");
+ if (op->request_max_len)
+ printf("\trequest %llu",
+ (unsigned long long)op->request_max_len);
+ if (op->action_reply_max_len)
+ printf("\treply %llu",
+ (unsigned long long)op->action_reply_max_len);
+ if (op->query_reply_min_len)
+ printf("\treply %llu..%llu",
+ (unsigned long long)op->query_reply_min_len,
+ (unsigned long long)op->query_reply_suggested_len);
+ printf("\n");
+ return 0;
+}
+
+static int cmd_ops(int argc, char **argv)
+{
+#ifdef HAVE_JSON_C
+ struct json_object *json = NULL;
+ struct json_object *json_ops = NULL;
+#endif
+ struct ctlvctl_spec *spec = NULL;
+ struct ctlv_error error;
+ struct ctlv_dev *dev;
+ struct ctlv_ops *ops;
+ char text[LINE_LEN];
+ unsigned int i;
+ int ret;
+
+ if (argc != 1) {
+ help(stderr);
+ return -EINVAL;
+ }
+ ret = dev_open(argv[0], O_RDONLY, &dev);
+ if (ret)
+ return ret;
+ ret = spec_load(dev, &spec);
+ if (ret)
+ goto out;
+
+ ret = ctlv_ops_get(dev, &ops, &error);
+ if (ret) {
+ fprintf(stderr, "%s: op-get: %s\n", prog,
+ ctlv_error_str(&error, text, sizeof(text)));
+ goto out;
+ }
+#ifdef HAVE_JSON_C
+ if (options.output_json) {
+ json = json_object_new_object();
+ json_ops = json_object_new_array();
+ if (!json || !json_ops) {
+ ret = -ENOMEM;
+ goto out_json;
+ }
+ json_add_string(json, "device", ctlv_dev_name(dev));
+ json_add_u64(json, "generation", ops->generation);
+ json_object_object_add(json, "ops", json_ops);
+ }
+#endif
+ if (!options.output_json) {
+ printf("# ");
+ chain_print(dev);
+ printf(", generation %llu\n",
+ (unsigned long long)ops->generation);
+ }
+ for (i = 0; i < ops->ops_count; i++) {
+#ifdef HAVE_JSON_C
+ if (json) {
+ const struct ctlvctl_op_name *name;
+ struct json_object *item;
+
+ name = ctlvctl_spec_op_id(spec, ops->ops[i].op_id);
+ if (name && !op_matches(name, &ops->ops[i])) {
+ fprintf(stderr,
+ "%s: specification disagrees with operation %#llx\n",
+ prog, (unsigned long long)
+ ops->ops[i].op_id);
+ ret = -ENOMSG;
+ break;
+ }
+ item = json_object_new_object();
+ if (!item) {
+ ret = -ENOMEM;
+ break;
+ }
+ if (name)
+ json_add_string(item, "name", name->name);
+ if (!name || options.show_ids)
+ json_add_id(item, "id", ops->ops[i].op_id);
+ json_add_string(item, "type",
+ type_name(ops->ops[i].type));
+ if (ops->ops[i].fd_access) {
+ const char *access =
+ fd_access_name(ops->ops[i].fd_access);
+
+ json_add_string(item, "fd-access",
+ access);
+ }
+ if (ops->ops[i].availability) {
+ const char *availability;
+
+ availability = ops->ops[i].availability ==
+ CTLV_OP_AVAILABILITY_DYNAMIC ?
+ "dynamic" : "fixed";
+ json_add_string(item, "availability",
+ availability);
+ if (ops->ops[i].availability ==
+ CTLV_OP_AVAILABILITY_DYNAMIC)
+ json_add_bool(item, "disabled",
+ ops->ops[i].disabled);
+ }
+ json_object_array_add(json_ops, item);
+ continue;
+ }
+#endif
+ ret = op_print(spec, &ops->ops[i]);
+ if (ret)
+ break;
+ }
+#ifdef HAVE_JSON_C
+ if (!ret && json)
+ ret = json_print(json);
+ else if (json)
+ json_object_put(json);
+ json = NULL;
+ json_ops = NULL;
+out_json:
+ if (json)
+ json_object_put(json);
+ else if (json_ops)
+ json_object_put(json_ops);
+#endif
+ ctlv_ops_free(ops);
+out:
+ ctlvctl_spec_free(spec);
+ ctlv_close(dev);
+ return ret;
+}
+
+static const struct ctlvctl_field_name *
+field_name(const struct ctlvctl_spec *spec,
+ const struct ctlv_node_schema *node)
+{
+ const struct ctlvctl_field_name *name;
+
+ if (!node)
+ return NULL;
+ name = ctlvctl_spec_field(spec, node->type);
+ if (name && name->kind != node->kind)
+ return NULL;
+ return name;
+}
+
+static int tree_names_check(const struct ctlvctl_spec *spec,
+ const struct ctlv_node_schema *node)
+{
+ const struct ctlvctl_field_name *name;
+ __u64 known = 0;
+ unsigned int i;
+ int ret;
+
+ if (!node)
+ return 0;
+ name = ctlvctl_spec_field(spec, node->type);
+ if (name && name->kind != node->kind) {
+ fprintf(stderr,
+ "%s: specification schema kind for %s disagrees with the device\n",
+ prog, name->name);
+ return -ENOMSG;
+ }
+ if (name && name->definition) {
+ for (i = 0; i < name->definition->values_count; i++) {
+ __u64 value = name->definition->values[i].value;
+
+ known |= value;
+ if (name->kind == CTLV_SCHEMA_ENUM &&
+ (!(node->present & CTLV_NODE_SCHEMA_HAS_MIN) ||
+ !(node->present & CTLV_NODE_SCHEMA_HAS_MAX) ||
+ value < node->min || value > node->max)) {
+ fprintf(stderr,
+ "%s: specification values for %s disagree with the device\n",
+ prog, name->name);
+ return -ENOMSG;
+ }
+ }
+ if (name->kind == CTLV_SCHEMA_FLAGS &&
+ (!(node->present & CTLV_NODE_SCHEMA_HAS_FLAGS_MASK) ||
+ (known & ~node->flags_mask))) {
+ fprintf(stderr,
+ "%s: specification flags for %s disagree with the device\n",
+ prog, name->name);
+ return -ENOMSG;
+ }
+ }
+ for (i = 0; i < node->fields_count; i++) {
+ ret = tree_names_check(spec, &node->fields[i]);
+ if (ret)
+ return ret;
+ }
+ for (i = 0; i < node->optional_fields_count; i++) {
+ ret = tree_names_check(spec, &node->optional_fields[i]);
+ if (ret)
+ return ret;
+ }
+ return 0;
+}
+
+static void tree_print(const struct ctlvctl_spec *spec,
+ const struct ctlv_node_schema *node, unsigned int indent,
+ int required)
+{
+ char line[LINE_LEN];
+ const struct ctlvctl_field_name *name;
+ char *detail;
+ unsigned int i;
+
+ name = field_name(spec, node);
+ ctlv_schema_render(node, 0, line, sizeof(line));
+ if (!name || node->type == CTLV_TYPE_ROOT) {
+ printf("%*s%s\n", indent * 2, "", line);
+ } else {
+ detail = strchr(line + strlen("field "), ' ');
+ printf("%*s%s", indent * 2, "", name->name);
+ if (options.show_ids)
+ printf(" (%#018llx)",
+ (unsigned long long)node->type);
+ if (required >= 0)
+ printf(" %s", required ? "required" : "optional");
+ printf("%s", detail ? detail : "");
+ if (name->definition) {
+ unsigned int j;
+
+ printf(" {");
+ for (j = 0; j < name->definition->values_count; j++) {
+ printf("%s%s", j ? ", " : "",
+ name->definition->values[j].name);
+ if (options.show_ids)
+ printf("=%#llx",
+ (unsigned long long)
+ name->definition->values[j].value);
+ }
+ printf("}");
+ }
+ printf("\n");
+ }
+ for (i = 0; i < node->fields_count; i++)
+ tree_print(spec, &node->fields[i], indent + 1, true);
+ for (i = 0; i < node->optional_fields_count; i++)
+ tree_print(spec, &node->optional_fields[i], indent + 1, false);
+}
+
+static int cmd_schema(int argc, char **argv)
+{
+ const struct ctlvctl_op_name *op_name;
+ const struct ctlv_op_info *op_info;
+ const struct ctlv_node_schema *node;
+ struct ctlvctl_spec *spec = NULL;
+ struct ctlv_schema *schema;
+ struct ctlv_ops *ops;
+ struct ctlv_error error;
+ struct ctlv_dev *dev;
+ char text[LINE_LEN];
+ __u64 op_id;
+ __u32 op_type = 0;
+ bool template = false;
+ int ret;
+
+ if (argc && !strcmp(argv[0], "--json-template")) {
+ template = true;
+ argc--;
+ argv++;
+ }
+ if (argc != 2) {
+ help(stderr);
+ return -EINVAL;
+ }
+ ret = dev_open(argv[0], O_RDONLY, &dev);
+ if (ret)
+ return ret;
+ ret = spec_load(dev, &spec);
+ if (ret)
+ goto out;
+ ret = op_resolve(spec, argv[1], &op_id);
+ if (ret)
+ goto out;
+ op_name = ctlvctl_spec_op_id(spec, op_id);
+ if (spec) {
+ ret = ctlv_ops_get(dev, &ops, &error);
+ if (ret) {
+ fprintf(stderr, "%s: op-get: %s\n", prog,
+ ctlv_error_str(&error, text, sizeof(text)));
+ goto out;
+ }
+ op_info = ctlv_ops_find(ops, op_id);
+ if (!op_info) {
+ fprintf(stderr,
+ "%s: operation is not available on %s\n",
+ prog, ctlv_dev_name(dev));
+ ret = -ENOENT;
+ } else {
+ op_type = op_info->type;
+ if (op_name && !op_matches(op_name, op_info)) {
+ fprintf(stderr,
+ "%s: specification disagrees with operation %s\n",
+ prog, op_name->name);
+ ret = -ENOMSG;
+ }
+ }
+ ctlv_ops_free(ops);
+ if (ret)
+ goto out;
+ }
+
+ ret = ctlv_op_schema_get(dev, op_id, &schema, &error);
+ if (ret) {
+ fprintf(stderr, "%s: op-schema-get: %s\n", prog,
+ ctlv_error_str(&error, text, sizeof(text)));
+ goto out;
+ }
+ ret = tree_names_check(spec, ctlv_schema_request(schema));
+ if (!ret)
+ ret = tree_names_check(spec, ctlv_schema_reply(schema));
+ if (ret)
+ goto out_schema;
+ if (template) {
+ node = ctlv_schema_request(schema);
+ if (!spec) {
+ fprintf(stderr,
+ "%s: --json-template requires --spec-dir\n", prog);
+ ret = -EINVAL;
+ } else if (!node) {
+ printf("{}\n");
+ } else {
+#ifdef HAVE_JSON_C
+ ret = json_template_print(node, spec);
+#else
+ ret = -EOPNOTSUPP;
+#endif
+ }
+ goto out_schema;
+ }
+#ifdef HAVE_JSON_C
+ if (options.output_json) {
+ ret = json_schema_print(dev, spec, op_id, schema);
+ goto out_schema;
+ }
+#endif
+ if (op_name) {
+ printf("%s (%s", op_name->name, type_name(op_name->type));
+ if (options.show_ids)
+ printf(", %#018llx", (unsigned long long)op_id);
+ printf(")\n");
+ } else if (spec) {
+ printf("%#018llx (%s)\n", (unsigned long long)op_id,
+ type_name(op_type));
+ }
+ node = ctlv_schema_request(schema);
+ if (node) {
+ printf("request:\n");
+ tree_print(spec, node, 1, -1);
+ }
+ node = ctlv_schema_reply(schema);
+ if (node) {
+ printf("reply:\n");
+ tree_print(spec, node, 1, -1);
+ }
+out_schema:
+ ctlv_schema_free(schema);
+out:
+ ctlvctl_spec_free(spec);
+ ctlv_close(dev);
+ return ret;
+}
+
+/*
+ * An assignment names a field by its type and a dotted path enters a record.
+ * What a value means is decided by the schema kind the device reports.
+ */
+
+/* The member of @parent for @type, following any re-entry. */
+static const struct ctlv_node_schema *
+node_member(const struct ctlv_node_schema **stack, unsigned int depth,
+ __u64 type)
+{
+ const struct ctlv_node_schema *parent = stack[depth];
+ unsigned int level = depth;
+ unsigned int i;
+
+ if (!parent)
+ return NULL;
+ while (parent->reenters) {
+ if (parent->reenters > level)
+ return NULL;
+ level -= parent->reenters;
+ parent = stack[level];
+ if (!parent)
+ return NULL;
+ }
+ for (i = 0; i < parent->fields_count; i++)
+ if (parent->fields[i].type == type)
+ return &parent->fields[i];
+ for (i = 0; i < parent->optional_fields_count; i++)
+ if (parent->optional_fields[i].type == type)
+ return &parent->optional_fields[i];
+ return NULL;
+}
+
+static int hex_digit(char c)
+{
+ if (c >= '0' && c <= '9')
+ return c - '0';
+ if (c >= 'a' && c <= 'f')
+ return c - 'a' + 10;
+ if (c >= 'A' && c <= 'F')
+ return c - 'A' + 10;
+ return -EINVAL;
+}
+
+static int put_hex(struct ctlv_msg *msg, __u64 type, const char *text)
+{
+ size_t text_len = strlen(text);
+ size_t len = text_len / 2;
+ unsigned char *bytes;
+ size_t i;
+ int ret;
+
+ if (text_len & 1)
+ return -EINVAL;
+ bytes = malloc(len ? len : 1);
+ if (!bytes)
+ return -ENOMEM;
+ for (i = 0; i < len; i++) {
+ int high = hex_digit(text[i * 2]);
+ int low = hex_digit(text[i * 2 + 1]);
+
+ if (high < 0 || low < 0) {
+ free(bytes);
+ return -EINVAL;
+ }
+ bytes[i] = high << 4 | low;
+ }
+ ret = ctlv_put_bin(msg, type, bytes, len);
+ free(bytes);
+ return ret;
+}
+
+static int flags_parse(const struct ctlvctl_field_name *field, const char *text,
+ __u64 *value)
+{
+ char *copy, *part, *walk;
+ int ret = 0;
+
+ if (!parse_u64(text, value))
+ return 0;
+ copy = strdup(text);
+ if (!copy)
+ return -ENOMEM;
+ *value = 0;
+ walk = copy;
+ while ((part = strsep(&walk, ","))) {
+ __u64 bit;
+
+ if (!*part || ctlvctl_spec_value(field, part, &bit)) {
+ ret = -EINVAL;
+ break;
+ }
+ *value |= bit;
+ }
+ free(copy);
+ return ret;
+}
+
+static int put_value(struct ctlv_msg *msg, const struct ctlv_node_schema *node,
+ const struct ctlvctl_spec *spec, const char *text)
+{
+ const struct ctlvctl_field_name *field = field_name(spec, node);
+ __u64 value;
+ __s64 signed_value;
+
+ switch (node->kind) {
+ case CTLV_SCHEMA_UINT:
+ if (parse_u64(text, &value))
+ return -EINVAL;
+ return ctlv_put_u64(msg, node->type, value);
+ case CTLV_SCHEMA_ENUM:
+ if (parse_u64(text, &value) &&
+ ctlvctl_spec_value(field, text, &value))
+ return -EINVAL;
+ return ctlv_put_u64(msg, node->type, value);
+ case CTLV_SCHEMA_FLAGS:
+ if (flags_parse(field, text, &value))
+ return -EINVAL;
+ return ctlv_put_u64(msg, node->type, value);
+ case CTLV_SCHEMA_SINT:
+ if (parse_s64(text, &signed_value))
+ return -EINVAL;
+ return ctlv_put_s64(msg, node->type, signed_value);
+ case CTLV_SCHEMA_BOOL:
+ if (!strcmp(text, "true") || !strcmp(text, "1"))
+ return ctlv_put_bool(msg, node->type, true);
+ if (!strcmp(text, "false") || !strcmp(text, "0"))
+ return ctlv_put_bool(msg, node->type, false);
+ return -EINVAL;
+ case CTLV_SCHEMA_STRING:
+ return ctlv_put_str(msg, node->type, text);
+ case CTLV_SCHEMA_BINARY:
+ if (field) {
+ if (strncmp(text, "hex:", 4))
+ return -EINVAL;
+ return put_hex(msg, node->type, text + 4);
+ }
+ return ctlv_put_bin(msg, node->type, text, strlen(text));
+ default:
+ /* A command line has no memory of the caller's to describe. */
+ return -ENOTSUP;
+ }
+}
+
+static const struct ctlv_node_schema *
+node_named(const struct ctlv_node_schema *parent,
+ const struct ctlvctl_spec *spec, const char *name, bool *ambiguous)
+{
+ const struct ctlv_node_schema *match = NULL;
+ unsigned int i;
+
+ *ambiguous = false;
+ for (i = 0; i < parent->fields_count + parent->optional_fields_count; i++) {
+ const struct ctlv_node_schema *node;
+ const struct ctlvctl_field_name *field;
+
+ if (i < parent->fields_count)
+ node = &parent->fields[i];
+ else
+ node = &parent->optional_fields[i - parent->fields_count];
+ field = field_name(spec, node);
+ if (!field)
+ continue;
+ if (!strcmp(field->name, name))
+ return node;
+ if (strcmp(field->short_name, name))
+ continue;
+ if (match)
+ *ambiguous = true;
+ match = node;
+ }
+ return *ambiguous ? NULL : match;
+}
+
+/* One path element: a type and which occurrence of it. */
+static int path_element(char *name, __u64 *type, __u64 *occurrence)
+{
+ char *at = strchr(name, '@');
+
+ *occurrence = 0;
+ if (at) {
+ *at++ = '\0';
+ if (parse_u64(at, occurrence))
+ return -EINVAL;
+ }
+ return parse_u64(name, type);
+}
+
+static int no_such_field(__u64 type)
+{
+ fprintf(stderr, "%s: %#llx: not a field of this record\n", prog,
+ (unsigned long long)type);
+ return -ENOENT;
+}
+
+static int not_a_record(__u64 type)
+{
+ fprintf(stderr, "%s: %#llx: not a record of this record\n", prog,
+ (unsigned long long)type);
+ return -ENOENT;
+}
+
+/**
+ * request_build - Turns assignments into one request.
+ * @msg: the message to append to
+ * @root: the request tree the device reported
+ * @spec: the names from --spec-dir, or NULL
+ * @argc: how many assignments
+ * @argv: the assignments, each field[@n][.field[@n]...]=value
+ *
+ * Records are opened as a path descends and closed as the next path leaves, so
+ * assignments have to be given in the order the message holds them. Two
+ * occurrences of one record are told apart by the @n suffix.
+ *
+ * Return: 0, or a negative errno naming the assignment that failed.
+ */
+static int request_build(struct ctlv_msg *msg,
+ const struct ctlv_node_schema *root,
+ const struct ctlvctl_spec *spec, int argc, char **argv)
+{
+ const struct ctlv_node_schema *stack[PATH_DEPTH] = { root };
+ __u64 path[PATH_DEPTH], occurrence[PATH_DEPTH];
+ unsigned int depth = 0;
+ int i, ret;
+
+ for (i = 0; i < argc; i++) {
+ char *text = strchr(argv[i], '=');
+ unsigned int level = 0;
+ char *name, *rest;
+
+ if (!text) {
+ fprintf(stderr, "%s: %s: not an assignment\n", prog,
+ argv[i]);
+ return -EINVAL;
+ }
+ *text++ = '\0';
+ rest = argv[i];
+
+ if (spec) {
+ const struct ctlv_node_schema *node;
+ bool ambiguous;
+
+ node = node_named(root, spec, rest, &ambiguous);
+ if (ambiguous) {
+ fprintf(stderr, "%s: %s: ambiguous field\n",
+ prog, rest);
+ return -EINVAL;
+ }
+ if (node) {
+ if (node->kind == CTLV_SCHEMA_RECORD ||
+ node->kind == CTLV_SCHEMA_BLOB) {
+ fprintf(stderr,
+ "%s: %s: use --json for this field\n",
+ prog, rest);
+ return -ENOTSUP;
+ }
+ /* A named field is the root's. */
+ while (depth) {
+ ctlv_record_end(msg);
+ depth--;
+ }
+ ret = put_value(msg, node, spec, text);
+ if (ret) {
+ fprintf(stderr, "%s: %s=%s: %s\n",
+ prog, rest, text,
+ strerror(-ret));
+ return ret;
+ }
+ continue;
+ }
+ }
+
+ while ((name = strsep(&rest, "."))) {
+ const struct ctlv_node_schema *node;
+ __u64 type, which;
+
+ if (level == PATH_DEPTH - 1)
+ return -E2BIG;
+ if (path_element(name, &type, &which)) {
+ fprintf(stderr, "%s: %s: not a type\n",
+ prog, name);
+ return -EINVAL;
+ }
+
+ /*
+ * A different record here, or the same one named again
+ * as a second occurrence, ends what came before.
+ */
+ if (level < depth && (path[level] != type ||
+ occurrence[level] != which))
+ while (depth > level) {
+ ctlv_record_end(msg);
+ depth--;
+ }
+
+ if (!rest) {
+ /* The leaf, which carries the value. */
+ while (depth > level) {
+ ctlv_record_end(msg);
+ depth--;
+ }
+ node = node_member(stack, depth, type);
+ if (!node)
+ return no_such_field(type);
+ ret = put_value(msg, node, spec, text);
+ if (ret) {
+ fprintf(stderr,
+ "%s: %#llx=%s: %s\n",
+ prog,
+ (unsigned long long)type,
+ text,
+ strerror(-ret));
+ return ret;
+ }
+ break;
+ }
+
+ if (level == depth) {
+ node = node_member(stack, depth, type);
+ if (!node)
+ return no_such_field(type);
+ if (node->kind != CTLV_SCHEMA_RECORD)
+ return not_a_record(type);
+ ret = ctlv_record_start(msg, type);
+ if (ret)
+ return ret;
+ path[depth] = type;
+ occurrence[depth] = which;
+ stack[depth + 1] = node;
+ depth++;
+ }
+ level++;
+ }
+ }
+ return ctlv_msg_err(msg);
+}
+
+static int stream_read(FILE *file, char **textp, size_t *lenp)
+{
+ size_t used = 0, size = 4096;
+ char *text;
+
+ text = malloc(size);
+ if (!text)
+ return -ENOMEM;
+ for (;;) {
+ size_t n;
+
+ if (used == size - 1) {
+ char *grown;
+
+ size *= 2;
+ grown = realloc(text, size);
+ if (!grown) {
+ free(text);
+ return -ENOMEM;
+ }
+ text = grown;
+ }
+ n = fread(text + used, 1, size - used - 1, file);
+ used += n;
+ if (n)
+ continue;
+ if (feof(file))
+ break;
+ if (errno == EINTR) {
+ clearerr(file);
+ continue;
+ }
+ free(text);
+ return -errno;
+ }
+ text[used] = '\0';
+ *textp = text;
+ *lenp = used;
+ return 0;
+}
+
+static int file_read(const char *path, char **textp, size_t *lenp)
+{
+ FILE *file;
+ int ret;
+
+ file = fopen(path, "re");
+ if (!file)
+ return -errno;
+ ret = stream_read(file, textp, lenp);
+ fclose(file);
+ return ret;
+}
+
+static int json_source_read(const char *source, char **textp, size_t *lenp)
+{
+ if (source[0] != '@' && strcmp(source, "-")) {
+ *textp = strdup(source);
+ if (!*textp)
+ return -ENOMEM;
+ *lenp = strlen(*textp);
+ return 0;
+ }
+ if (!strcmp(source, "-"))
+ return stream_read(stdin, textp, lenp);
+ return file_read(source + 1, textp, lenp);
+}
+
+/* A blob --json names by its file, and the memory its descriptor names. */
+struct blob_file {
+ const struct ctlv_node_schema *node;
+ char *path;
+ void *data;
+ __u64 len;
+ /* Where its descriptor starts in the request. */
+ __u64 offset;
+ /* Whether the length is settled, rather than the op's to ask for. */
+ bool sized;
+ unsigned int grown;
+};
+
+/* A rebuilt request meets its blobs in the order the first build did. */
+struct blob_files {
+ struct blob_file *files;
+ unsigned int count;
+ /* How many of them the request being built has reached. */
+ unsigned int used;
+};
+
+/* How often one blob grows for an op that keeps asking for more. */
+#define BLOB_GROWN_MAX 2
+
+/* Zeroed and aligned as the blob requires. */
+static int blob_alloc(struct blob_file *blob, __u64 len)
+{
+ size_t align = blob->node->blob_alignment;
+ void *data;
+ int ret;
+
+ if (align < sizeof(void *))
+ align = sizeof(void *);
+ if (len > SIZE_MAX)
+ return -EFBIG;
+ ret = posix_memalign(&data, align, len);
+ if (ret)
+ return -ret;
+ memset(data, 0, len);
+ free(blob->data);
+ blob->data = data;
+ blob->len = len;
+ return 0;
+}
+
+/*
+ * Whether the op failed for want of a longer output blob that ctlvctl sizes,
+ * which then grows so that the op can be retried.
+ */
+static bool blobs_grow(struct blob_files *blobs, const struct ctlv_error *error)
+{
+ const __u32 need = CTLV_ERROR_HAS_BAD_TLV_OFFSET |
+ CTLV_ERROR_HAS_REQUIRED_LENGTH;
+ unsigned int i;
+
+ if (error->err != -EMSGSIZE || (error->present & need) != need)
+ return false;
+ for (i = 0; i < blobs->count; i++) {
+ struct blob_file *blob = &blobs->files[i];
+
+ if (blob->offset != error->bad_tlv_offset)
+ continue;
+ if (blob->sized || blob->grown == BLOB_GROWN_MAX ||
+ error->required_length <= blob->len ||
+ error->required_length > blob->node->blob_max_length)
+ return false;
+ blob->grown++;
+ return !blob_alloc(blob, error->required_length);
+ }
+ return false;
+}
+
+/* The device does not say how much it wrote, so a file gets the whole blob. */
+static int blobs_save(const struct blob_files *blobs)
+{
+ unsigned int i;
+
+ for (i = 0; i < blobs->count; i++) {
+ const struct blob_file *blob = &blobs->files[i];
+ FILE *file;
+ int ret = 0;
+
+ if (blob->node->blob_direction == CTLV_BLOB_DIRECTION_INPUT)
+ continue;
+ file = fopen(blob->path, "we");
+ if (!file)
+ ret = -errno;
+ else if (fwrite(blob->data, 1, blob->len, file) != blob->len)
+ ret = errno ? -errno : -EIO;
+ if (file && fclose(file) && !ret)
+ ret = errno ? -errno : -EIO;
+ if (ret) {
+ fprintf(stderr, "%s: %s: %s\n", prog, blob->path,
+ strerror(-ret));
+ return ret;
+ }
+ }
+ return 0;
+}
+
+static void blobs_free(struct blob_files *blobs)
+{
+ unsigned int i;
+
+ for (i = 0; i < blobs->count; i++) {
+ free(blobs->files[i].path);
+ free(blobs->files[i].data);
+ }
+ free(blobs->files);
+}
+
+#ifdef HAVE_JSON_C
+
+static const struct ctlv_node_schema *
+node_key(const struct ctlv_node_schema *parent,
+ const struct ctlvctl_spec *spec, const char *key, bool *ambiguous)
+{
+ __u64 id;
+
+ if (!parse_u64(key, &id))
+ return node_member((const struct ctlv_node_schema *[]){ parent },
+ 0, id);
+ return node_named(parent, spec, key, ambiguous);
+}
+
+static int json_node_value(struct json_object *object,
+ const struct ctlv_node_schema *parent,
+ const struct ctlv_node_schema *wanted,
+ const struct ctlvctl_spec *spec,
+ struct json_object **value, bool *present)
+{
+ struct json_object *found = NULL;
+
+ json_object_object_foreach(object, key, item) {
+ const struct ctlv_node_schema *node;
+ bool ambiguous = false;
+
+ node = node_key(parent, spec, key, &ambiguous);
+ if (ambiguous) {
+ fprintf(stderr, "%s: %s: ambiguous field\n",
+ prog, key);
+ return -EINVAL;
+ }
+ if (!node) {
+ fprintf(stderr, "%s: %s: no such field here\n",
+ prog, key);
+ return -ENOENT;
+ }
+ if (node != wanted)
+ continue;
+ if (found) {
+ fprintf(stderr, "%s: %s: field given twice\n",
+ prog, key);
+ return -EINVAL;
+ }
+ found = item;
+ }
+ *present = !!found;
+ *value = found;
+ return 0;
+}
+
+static int json_text_value(struct json_object *value, const char **text)
+{
+ if (!json_object_is_type(value, json_type_string))
+ return -EINVAL;
+ *text = json_object_get_string(value);
+ if (strlen(*text) != (size_t)json_object_get_string_len(value))
+ return -EINVAL;
+ return 0;
+}
+
+static int json_u64_value(struct json_object *value, __u64 *number)
+{
+ const char *text;
+
+ if (json_object_is_type(value, json_type_int)) {
+ text = json_object_get_string(value);
+ return parse_u64(text, number);
+ }
+ if (!json_text_value(value, &text))
+ return parse_u64(text, number);
+ return -EINVAL;
+}
+
+static int json_s64_value(struct json_object *value, __s64 *number)
+{
+ if (json_object_is_type(value, json_type_int))
+ return parse_s64(json_object_get_string(value), number);
+ {
+ const char *text;
+
+ if (json_text_value(value, &text))
+ return -EINVAL;
+ return parse_s64(text, number);
+ }
+}
+
+static int json_flags_value(const struct ctlvctl_field_name *field,
+ struct json_object *json, __u64 *value)
+{
+ size_t i, n;
+
+ if (!json_object_is_type(json, json_type_array))
+ return json_u64_value(json, value);
+ *value = 0;
+ n = json_object_array_length(json);
+ for (i = 0; i < n; i++) {
+ struct json_object *item = json_object_array_get_idx(json, i);
+ const char *name;
+ __u64 bit;
+
+ if (json_text_value(item, &name) ||
+ ctlvctl_spec_value(field, name, &bit))
+ return -EINVAL;
+ *value |= bit;
+ }
+ return 0;
+}
+
+static int json_put_value(struct ctlv_msg *msg,
+ const struct ctlv_node_schema *node,
+ const struct ctlvctl_spec *spec,
+ struct json_object *json)
+{
+ const struct ctlvctl_field_name *field = field_name(spec, node);
+ const char *text;
+ __u64 value;
+ __s64 signed_value;
+
+ switch (node->kind) {
+ case CTLV_SCHEMA_UINT:
+ if (json_u64_value(json, &value))
+ return -EINVAL;
+ return ctlv_put_u64(msg, node->type, value);
+ case CTLV_SCHEMA_SINT:
+ if (json_s64_value(json, &signed_value))
+ return -EINVAL;
+ return ctlv_put_s64(msg, node->type, signed_value);
+ case CTLV_SCHEMA_BOOL:
+ if (!json_object_is_type(json, json_type_boolean))
+ return -EINVAL;
+ return ctlv_put_bool(msg, node->type,
+ json_object_get_boolean(json));
+ case CTLV_SCHEMA_STRING:
+ if (json_text_value(json, &text))
+ return -EINVAL;
+ return ctlv_put_str(msg, node->type, text);
+ case CTLV_SCHEMA_BINARY:
+ if (json_text_value(json, &text))
+ return -EINVAL;
+ if (strncmp(text, "hex:", 4))
+ return -EINVAL;
+ return put_hex(msg, node->type, text + 4);
+ case CTLV_SCHEMA_ENUM:
+ if (json_object_is_type(json, json_type_string)) {
+ if (json_text_value(json, &text) ||
+ ctlvctl_spec_value(field, text, &value))
+ return -EINVAL;
+ return ctlv_put_u64(msg, node->type, value);
+ }
+ if (json_u64_value(json, &value))
+ return -EINVAL;
+ return ctlv_put_u64(msg, node->type, value);
+ case CTLV_SCHEMA_FLAGS:
+ if (json_flags_value(field, json, &value))
+ return -EINVAL;
+ return ctlv_put_u64(msg, node->type, value);
+ default:
+ return -ENOTSUP;
+ }
+}
+
+static const struct ctlv_node_schema *
+node_members(const struct ctlv_node_schema **stack, unsigned int depth)
+{
+ const struct ctlv_node_schema *node = stack[depth];
+ unsigned int level = depth;
+
+ if (!node)
+ return NULL;
+ while (node->reenters) {
+ if (node->reenters > level)
+ return NULL;
+ level -= node->reenters;
+ node = stack[level];
+ if (!node)
+ return NULL;
+ }
+ return node;
+}
+
+/* {"file": PATH}, and "length" for a blob the op writes. */
+static int json_blob_parse(const struct ctlv_node_schema *node,
+ struct json_object *value, const char **path,
+ __u64 *len, bool *sized)
+{
+ struct json_object *file = NULL, *length = NULL;
+
+ if (!json_object_is_type(value, json_type_object))
+ return -EINVAL;
+ json_object_object_foreach(value, key, item) {
+ if (!strcmp(key, "file"))
+ file = item;
+ else if (!strcmp(key, "length"))
+ length = item;
+ else
+ return -EINVAL;
+ }
+ if (!file || json_text_value(file, path) || !**path)
+ return -EINVAL;
+ *sized = !!length;
+ if (!length)
+ return 0;
+ if (node->blob_direction == CTLV_BLOB_DIRECTION_INPUT)
+ return -EINVAL;
+ return json_u64_value(length, len);
+}
+
+/* An output blob starts at a byte unless the request says how long it is. */
+static int json_blob_new(const struct ctlv_node_schema *node,
+ struct json_object *value, struct blob_files *blobs)
+{
+ bool output = node->blob_direction == CTLV_BLOB_DIRECTION_OUTPUT;
+ struct blob_file *files, *blob;
+ char *contents = NULL;
+ const char *path;
+ size_t size = 0;
+ __u64 len = 1;
+ bool sized;
+ int ret;
+
+ if (!(node->present & CTLV_NODE_SCHEMA_HAS_BLOB) ||
+ !(node->blob_backings & (1ULL << CTLV_BLOB_TYPE_VA)))
+ return -EOPNOTSUPP;
+ ret = json_blob_parse(node, value, &path, &len, &sized);
+ if (ret)
+ return ret;
+ if (!output) {
+ ret = file_read(path, &contents, &size);
+ if (ret)
+ return ret;
+ if (!sized)
+ len = size;
+ }
+ ret = -EINVAL;
+ if (!len)
+ goto out;
+ ret = -EFBIG;
+ if (len > node->blob_max_length || size > len)
+ goto out;
+ ret = -ENOMEM;
+ files = realloc(blobs->files, (blobs->count + 1) * sizeof(*files));
+ if (!files)
+ goto out;
+ blobs->files = files;
+ blob = &files[blobs->count];
+ *blob = (struct blob_file){
+ .node = node,
+ .path = strdup(path),
+ .sized = sized || !output,
+ };
+ if (!blob->path)
+ goto out;
+ ret = blob_alloc(blob, len);
+ if (ret) {
+ free(blob->path);
+ goto out;
+ }
+ if (size)
+ memcpy(blob->data, contents, size);
+ blobs->count++;
+out:
+ free(contents);
+ return ret;
+}
+
+static int json_blob_put(struct ctlv_msg *msg,
+ const struct ctlv_node_schema *node,
+ struct json_object *value, struct blob_files *blobs)
+{
+ struct blob_file *blob;
+ int ret;
+
+ if (blobs->used == blobs->count) {
+ ret = json_blob_new(node, value, blobs);
+ if (ret)
+ return ret;
+ }
+ blob = &blobs->files[blobs->used++];
+ if (blob->node != node)
+ return -EINVAL;
+ blob->offset = CTLV_ALIGN(ctlv_msg_len(msg));
+ return ctlv_put_blob_va(msg, node->type, blob->data, blob->len);
+}
+
+static int json_build_object(struct ctlv_msg *msg,
+ const struct ctlv_node_schema **stack,
+ unsigned int depth, struct json_object *object,
+ const struct ctlvctl_spec *spec,
+ struct blob_files *blobs);
+
+static int json_build_one(struct ctlv_msg *msg,
+ const struct ctlv_node_schema **stack,
+ unsigned int depth,
+ const struct ctlv_node_schema *node,
+ struct json_object *value,
+ const struct ctlvctl_spec *spec,
+ struct blob_files *blobs)
+{
+ int ret;
+
+ if (node->kind == CTLV_SCHEMA_BLOB)
+ return json_blob_put(msg, node, value, blobs);
+ if (node->kind != CTLV_SCHEMA_RECORD)
+ return json_put_value(msg, node, spec, value);
+ if (!json_object_is_type(value, json_type_object))
+ return -EINVAL;
+ if (depth + 1 >= PATH_DEPTH)
+ return -E2BIG;
+
+ ret = ctlv_record_start(msg, node->type);
+ if (ret)
+ return ret;
+ stack[depth + 1] = node;
+ ret = json_build_object(msg, stack, depth + 1, value, spec, blobs);
+ ctlv_record_end(msg);
+ return ret;
+}
+
+static int json_build_node(struct ctlv_msg *msg,
+ const struct ctlv_node_schema **stack,
+ unsigned int depth,
+ const struct ctlv_node_schema *node,
+ struct json_object *value,
+ const struct ctlvctl_spec *spec,
+ struct blob_files *blobs)
+{
+ size_t i, n;
+ int ret;
+
+ if (node->max_occurrences == 1)
+ return json_build_one(msg, stack, depth, node, value, spec,
+ blobs);
+ if (!json_object_is_type(value, json_type_array))
+ return -EINVAL;
+ n = json_object_array_length(value);
+ if (node->max_occurrences && n > node->max_occurrences)
+ return -E2BIG;
+ for (i = 0; i < n; i++) {
+ ret = json_build_one(msg, stack, depth, node,
+ json_object_array_get_idx(value, i), spec,
+ blobs);
+ if (ret)
+ return ret;
+ }
+ return 0;
+}
+
+static int json_build_members(struct ctlv_msg *msg,
+ const struct ctlv_node_schema **stack,
+ unsigned int depth,
+ const struct ctlv_node_schema *parent,
+ const struct ctlv_node_schema *node,
+ unsigned int n, bool required,
+ struct json_object *object,
+ const struct ctlvctl_spec *spec,
+ struct blob_files *blobs)
+{
+ unsigned int i;
+
+ for (i = 0; i < n; i++) {
+ const struct ctlvctl_field_name *name = field_name(spec, &node[i]);
+ struct json_object *value;
+ bool present;
+ int ret;
+
+ ret = json_node_value(object, parent, &node[i], spec,
+ &value, &present);
+ if (ret)
+ return ret;
+ if (!present) {
+ if (!required)
+ continue;
+ fprintf(stderr, "%s: %s: required field is missing\n",
+ prog, name ? name->name : "field");
+ return -EINVAL;
+ }
+ if (required && node[i].max_occurrences != 1 &&
+ json_object_is_type(value, json_type_array) &&
+ !json_object_array_length(value)) {
+ fprintf(stderr, "%s: %s: at least one value is required\n",
+ prog, name ? name->name : "field");
+ return -EINVAL;
+ }
+ ret = json_build_node(msg, stack, depth, &node[i], value,
+ spec, blobs);
+ if (ret) {
+ fprintf(stderr, "%s: %s: %s\n", prog,
+ name ? name->name : "field", strerror(-ret));
+ return ret;
+ }
+ }
+ return 0;
+}
+
+static int json_build_object(struct ctlv_msg *msg,
+ const struct ctlv_node_schema **stack,
+ unsigned int depth, struct json_object *object,
+ const struct ctlvctl_spec *spec,
+ struct blob_files *blobs)
+{
+ const struct ctlv_node_schema *parent;
+ int ret;
+
+ if (!json_object_is_type(object, json_type_object))
+ return -EINVAL;
+ parent = node_members(stack, depth);
+ if (!parent)
+ return -EINVAL;
+ json_object_object_foreach(object, key, item) {
+ bool ambiguous = false;
+
+ if (!node_key(parent, spec, key, &ambiguous)) {
+ fprintf(stderr, "%s: %s: %s field\n", prog, key,
+ ambiguous ? "ambiguous" : "no such");
+ return ambiguous ? -EINVAL : -ENOENT;
+ }
+ }
+
+ ret = json_build_members(msg, stack, depth, parent, parent->fields,
+ parent->fields_count, true, object, spec,
+ blobs);
+ if (ret)
+ return ret;
+ return json_build_members(msg, stack, depth, parent,
+ parent->optional_fields,
+ parent->optional_fields_count, false, object,
+ spec, blobs);
+}
+
+/* A rebuild for a retry parses @text again and starts the message over. */
+static int json_request_build(struct ctlv_msg *msg,
+ const struct ctlv_node_schema *root,
+ const struct ctlvctl_spec *spec,
+ const char *text, size_t text_len,
+ struct blob_files *blobs)
+{
+ const struct ctlv_node_schema *stack[PATH_DEPTH] = { root };
+ struct json_tokener *tokener;
+ struct json_object *object;
+ enum json_tokener_error error;
+ int ret;
+
+ if (memchr(text, '\0', text_len))
+ return -EINVAL;
+ if (text_len >= INT_MAX)
+ return -E2BIG;
+ tokener = json_tokener_new_ex(PATH_DEPTH * 2 + 2);
+ if (!tokener)
+ return -ENOMEM;
+ json_tokener_set_flags(tokener, JSON_TOKENER_STRICT |
+ JSON_TOKENER_VALIDATE_UTF8);
+ object = json_tokener_parse_ex(tokener, text, text_len + 1);
+ error = json_tokener_get_error(tokener);
+ if (error != json_tokener_success || !object) {
+ fprintf(stderr, "%s: invalid JSON request: %s\n", prog,
+ json_tokener_error_desc(error));
+ ret = -EINVAL;
+ } else {
+ ctlv_msg_reset(msg);
+ blobs->used = 0;
+ ret = json_build_object(msg, stack, 0, object, spec, blobs);
+ json_object_put(object);
+ }
+ json_tokener_free(tokener);
+ return ret;
+}
+
+#else /* HAVE_JSON_C */
+
+static int json_request_build(struct ctlv_msg *msg,
+ const struct ctlv_node_schema *root,
+ const struct ctlvctl_spec *spec,
+ const char *text, size_t text_len,
+ struct blob_files *blobs)
+{
+ fprintf(stderr, "%s: JSON support was built without json-c\n", prog);
+ return -EOPNOTSUPP;
+}
+
+#endif /* HAVE_JSON_C */
+
+#ifdef HAVE_JSON_C
+
+static char *hex_encode(const void *bytes, __u32 len, const char *prefix)
+{
+ const unsigned char *data = bytes;
+ size_t prefix_len = strlen(prefix);
+ char *text;
+ __u32 i;
+
+ text = malloc(prefix_len + (size_t)len * 2 + 1);
+ if (!text)
+ return NULL;
+ memcpy(text, prefix, prefix_len);
+ for (i = 0; i < len; i++)
+ snprintf(text + prefix_len + i * 2, 3, "%02x", data[i]);
+ return text;
+}
+
+static struct json_object *json_binary(const void *bytes, __u32 len)
+{
+ struct json_object *json;
+ char *text;
+
+ text = hex_encode(bytes, len, "hex:");
+ if (!text)
+ return NULL;
+ json = json_object_new_string(text);
+ free(text);
+ return json;
+}
+
+static struct json_object *json_invalid_utf8(const void *bytes, __u32 len)
+{
+ struct json_object *json;
+ char *text;
+
+ json = json_object_new_object();
+ text = hex_encode(bytes, len, "");
+ if (!json || !text) {
+ json_object_put(json);
+ free(text);
+ return NULL;
+ }
+ json_add_string(json, "encoding", "hex");
+ json_add_string(json, "data", text);
+ free(text);
+ return json;
+}
+
+static bool utf8_valid(const unsigned char *text, size_t len)
+{
+ size_t i = 0;
+
+ while (i < len) {
+ unsigned char first = text[i++];
+ unsigned int continuation;
+
+ if (first < 0x80)
+ continue;
+ if (first >= 0xc2 && first <= 0xdf) {
+ continuation = 1;
+ } else if (first >= 0xe0 && first <= 0xef) {
+ continuation = 2;
+ if (i >= len ||
+ (first == 0xe0 && text[i] < 0xa0) ||
+ (first == 0xed && text[i] >= 0xa0))
+ return false;
+ } else if (first >= 0xf0 && first <= 0xf4) {
+ continuation = 3;
+ if (i >= len ||
+ (first == 0xf0 && text[i] < 0x90) ||
+ (first == 0xf4 && text[i] >= 0x90))
+ return false;
+ } else {
+ return false;
+ }
+ if (continuation > len - i)
+ return false;
+ while (continuation--)
+ if ((text[i++] & 0xc0) != 0x80)
+ return false;
+ }
+ return true;
+}
+
+static struct json_object *
+json_payload(const struct ctlv_hdr *hdr,
+ const struct ctlv_node_schema *node,
+ const struct ctlvctl_spec *spec)
+{
+ const struct ctlvctl_field_name *name = field_name(spec, node);
+ const void *bytes;
+ const char *text;
+ struct json_object *json;
+ __u64 value;
+ __s64 signed_value;
+ __u32 len;
+ bool flag;
+ unsigned int i;
+
+ switch (node ? node->kind : CTLV_SCHEMA_BINARY) {
+ case CTLV_SCHEMA_UINT:
+ if (ctlv_get_u64(hdr, &value))
+ return NULL;
+ return json_object_new_uint64(value);
+ case CTLV_SCHEMA_SINT:
+ if (ctlv_get_s64(hdr, &signed_value))
+ return NULL;
+ return json_object_new_int64(signed_value);
+ case CTLV_SCHEMA_BOOL:
+ if (ctlv_get_bool(hdr, &flag))
+ return NULL;
+ return json_object_new_boolean(flag);
+ case CTLV_SCHEMA_STRING:
+ if (ctlv_get_str(hdr, &text))
+ return NULL;
+ len = ctlv_hdr_payload_len(hdr) - 1;
+ if (!utf8_valid((const unsigned char *)text, len))
+ return json_invalid_utf8(text, len);
+ return json_object_new_string_len(text, len);
+ case CTLV_SCHEMA_ENUM:
+ if (ctlv_get_u64(hdr, &value))
+ return NULL;
+ text = ctlvctl_spec_value_name(name, value);
+ return text ? json_object_new_string(text) :
+ json_object_new_uint64(value);
+ case CTLV_SCHEMA_FLAGS:
+ if (ctlv_get_u64(hdr, &value))
+ return NULL;
+ if (!name || !name->definition)
+ return json_object_new_uint64(value);
+ json = json_object_new_array();
+ if (!json)
+ return NULL;
+ for (i = 0; i < name->definition->values_count; i++) {
+ const struct ctlvctl_value_name *item =
+ &name->definition->values[i];
+
+ if (!(value & item->value))
+ continue;
+ if (json_array_add_string(json, item->name)) {
+ json_object_put(json);
+ return NULL;
+ }
+ value &= ~item->value;
+ }
+ if (value) {
+ char unknown[32];
+
+ snprintf(unknown, sizeof(unknown), "%#llx",
+ (unsigned long long)value);
+ if (json_array_add_string(json, unknown)) {
+ json_object_put(json);
+ return NULL;
+ }
+ }
+ return json;
+ default:
+ if (ctlv_get_bin(hdr, &bytes, &len))
+ return NULL;
+ return json_binary(bytes, len);
+ }
+}
+
+static int json_object_add_value(struct json_object *object, const char *key,
+ struct json_object *value, bool repeated)
+{
+ struct json_object *array, *existing;
+ int ret;
+
+ if (!json_object_object_get_ex(object, key, &existing) && !repeated) {
+ ret = json_object_object_add(object, key, value);
+ if (ret)
+ json_object_put(value);
+ return ret ? -ENOMEM : 0;
+ }
+ if (!json_object_object_get_ex(object, key, &existing)) {
+ array = json_object_new_array();
+ if (!array) {
+ json_object_put(value);
+ return -ENOMEM;
+ }
+ ret = json_object_object_add(object, key, array);
+ if (ret) {
+ json_object_put(array);
+ json_object_put(value);
+ return -ENOMEM;
+ }
+ ret = json_object_array_add(array, value);
+ if (ret)
+ json_object_put(value);
+ return ret ? -ENOMEM : 0;
+ }
+ if (json_object_is_type(existing, json_type_array)) {
+ ret = json_object_array_add(existing, value);
+ if (ret)
+ json_object_put(value);
+ return ret ? -ENOMEM : 0;
+ }
+
+ array = json_object_new_array();
+ if (!array) {
+ json_object_put(value);
+ return -ENOMEM;
+ }
+ json_object_get(existing);
+ if (json_object_array_add(array, existing)) {
+ json_object_put(existing);
+ json_object_put(array);
+ json_object_put(value);
+ return -ENOMEM;
+ }
+ if (json_object_object_add(object, key, array)) {
+ json_object_put(array);
+ json_object_put(value);
+ return -ENOMEM;
+ }
+ ret = json_object_array_add(array, value);
+ if (ret)
+ json_object_put(value);
+ return ret ? -ENOMEM : 0;
+}
+
+static struct json_object *
+json_reply_object(const struct ctlv_hdr *record,
+ const struct ctlv_node_schema **stack,
+ unsigned int depth, const struct ctlvctl_spec *spec)
+{
+ struct json_object *object;
+ const struct ctlv_hdr *child;
+
+ object = json_object_new_object();
+ if (!object)
+ return NULL;
+ ctlv_for_each(child, record) {
+ const struct ctlv_node_schema *node = NULL;
+ const struct ctlvctl_field_name *name;
+ struct json_object *value;
+ char numeric[32];
+ const char *key;
+
+ if (depth < PATH_DEPTH)
+ node = node_member(stack, depth, child->type);
+ name = node ? field_name(spec, node) : NULL;
+ if (name) {
+ key = name->name;
+ } else {
+ snprintf(numeric, sizeof(numeric), "%#018llx",
+ (unsigned long long)child->type);
+ key = numeric;
+ }
+ if (ctlv_hdr_is_record(child)) {
+ if (depth + 1 >= PATH_DEPTH) {
+ json_object_put(object);
+ return NULL;
+ }
+ stack[depth + 1] = node;
+ value = json_reply_object(child, stack, depth + 1,
+ spec);
+ } else {
+ value = json_payload(child, node, spec);
+ }
+ if (!value ||
+ json_object_add_value(object, key, value,
+ node &&
+ node->max_occurrences != 1)) {
+ json_object_put(object);
+ return NULL;
+ }
+ }
+ return object;
+}
+
+static struct json_object *
+json_reply(const struct ctlv_hdr *root,
+ const struct ctlv_node_schema *tree,
+ const struct ctlvctl_spec *spec)
+{
+ const struct ctlv_node_schema *stack[PATH_DEPTH] = { tree };
+
+ return json_reply_object(root, stack, 0, spec);
+}
+
+static void json_add_id(struct json_object *object, const char *key, __u64 id)
+{
+ char text[32];
+
+ snprintf(text, sizeof(text), "%#018llx", (unsigned long long)id);
+ json_object_object_add(object, key, json_object_new_string(text));
+}
+
+static void json_add_op(struct json_object *object,
+ const struct ctlvctl_spec *spec, __u64 op_id)
+{
+ const struct ctlvctl_op_name *name = ctlvctl_spec_op_id(spec, op_id);
+
+ if (name)
+ json_add_string(object, "op", name->name);
+ if (!name || options.show_ids)
+ json_add_id(object, "id", op_id);
+}
+
+static const char *schema_kind_name(__u32 kind)
+{
+ static const char *const names[] = {
+ [CTLV_SCHEMA_UINT] = "uint",
+ [CTLV_SCHEMA_SINT] = "sint",
+ [CTLV_SCHEMA_BOOL] = "bool",
+ [CTLV_SCHEMA_STRING] = "string",
+ [CTLV_SCHEMA_BINARY] = "binary",
+ [CTLV_SCHEMA_ENUM] = "enum",
+ [CTLV_SCHEMA_FLAGS] = "flags",
+ [CTLV_SCHEMA_RECORD] = "record",
+ [CTLV_SCHEMA_BLOB] = "blob",
+ };
+
+ if (kind >= ARRAY_SIZE(names) || !names[kind])
+ return "unknown";
+ return names[kind];
+}
+
+static struct json_object *
+json_schema_node(const struct ctlv_node_schema *node,
+ const struct ctlvctl_spec *spec, bool required)
+{
+ const struct ctlvctl_field_name *name = field_name(spec, node);
+ struct json_object *object, *array;
+ unsigned int i;
+
+ object = json_object_new_object();
+ if (!object)
+ return NULL;
+ if (node->type != CTLV_TYPE_ROOT) {
+ if (name)
+ json_add_string(object, "name", name->name);
+ if (!name || options.show_ids)
+ json_add_id(object, "type", node->type);
+ json_add_bool(object, "required", required);
+ }
+ json_add_string(object, "kind", schema_kind_name(node->kind));
+ if (!node->max_occurrences)
+ json_add_string(object, "max-occurrences", "unlimited");
+ else
+ json_add_u64(object, "max-occurrences",
+ node->max_occurrences);
+ if (node->present & CTLV_NODE_SCHEMA_HAS_MIN) {
+ if (node->kind == CTLV_SCHEMA_SINT)
+ json_add_s64(object, "min", (__s64)node->min);
+ else
+ json_add_u64(object, "min", node->min);
+ }
+ if (node->present & CTLV_NODE_SCHEMA_HAS_MAX) {
+ if (node->kind == CTLV_SCHEMA_SINT)
+ json_add_s64(object, "max", (__s64)node->max);
+ else
+ json_add_u64(object, "max", node->max);
+ }
+ if (node->present & CTLV_NODE_SCHEMA_HAS_MIN_LENGTH)
+ json_add_u64(object, "min-length", node->min_length);
+ if (node->present & CTLV_NODE_SCHEMA_HAS_MAX_LENGTH)
+ json_add_u64(object, "max-length", node->max_length);
+ if (name && name->definition) {
+ array = json_object_new_array();
+ if (!array) {
+ json_object_put(object);
+ return NULL;
+ }
+ for (i = 0; i < name->definition->values_count; i++) {
+ const char *value_name =
+ name->definition->values[i].name;
+
+ if (json_array_add_string(array, value_name)) {
+ json_object_put(array);
+ json_object_put(object);
+ return NULL;
+ }
+ }
+ json_object_object_add(object, "values", array);
+ }
+ if (node->reenters)
+ json_add_u64(object, "reenters", node->reenters);
+
+ array = json_object_new_array();
+ if (!array) {
+ json_object_put(object);
+ return NULL;
+ }
+ for (i = 0; i < node->fields_count; i++) {
+ struct json_object *child =
+ json_schema_node(&node->fields[i], spec, true);
+
+ if (!child) {
+ json_object_put(array);
+ json_object_put(object);
+ return NULL;
+ }
+ json_object_array_add(array, child);
+ }
+ for (i = 0; i < node->optional_fields_count; i++) {
+ struct json_object *child =
+ json_schema_node(&node->optional_fields[i], spec, false);
+
+ if (!child) {
+ json_object_put(array);
+ json_object_put(object);
+ return NULL;
+ }
+ json_object_array_add(array, child);
+ }
+ if (json_object_array_length(array))
+ json_object_object_add(object, "fields", array);
+ else
+ json_object_put(array);
+ return object;
+}
+
+static int json_schema_print(const struct ctlv_dev *dev,
+ const struct ctlvctl_spec *spec, __u64 op_id,
+ const struct ctlv_schema *schema)
+{
+ const struct ctlv_node_schema *node;
+ struct json_object *object, *tree;
+
+ object = json_object_new_object();
+ if (!object)
+ return -ENOMEM;
+ json_add_string(object, "device", ctlv_dev_name(dev));
+ json_add_op(object, spec, op_id);
+ node = ctlv_schema_request(schema);
+ if (node) {
+ tree = json_schema_node(node, spec, true);
+ if (!tree)
+ goto err;
+ json_object_object_add(object, "request", tree);
+ }
+ node = ctlv_schema_reply(schema);
+ if (node) {
+ tree = json_schema_node(node, spec, true);
+ if (!tree)
+ goto err;
+ json_object_object_add(object, "reply", tree);
+ }
+ return json_print(object);
+err:
+ json_object_put(object);
+ return -ENOMEM;
+}
+
+static int json_print(struct json_object *object)
+{
+ const char *text;
+ size_t len;
+ int ret = 0;
+
+ if (!object)
+ return -ENOMEM;
+ text = json_object_to_json_string_ext(object,
+ JSON_C_TO_STRING_PLAIN);
+ if (!text) {
+ ret = -ENOMEM;
+ goto out;
+ }
+ len = strlen(text);
+ if (fwrite(text, 1, len, stdout) != len ||
+ fputc('\n', stdout) == EOF || fflush(stdout))
+ ret = errno ? -errno : -EIO;
+out:
+ json_object_put(object);
+ return ret;
+}
+
+static struct json_object *
+json_template_object(const struct ctlv_node_schema **stack,
+ unsigned int depth, const struct ctlvctl_spec *spec);
+
+static struct json_object *
+json_template_value(const struct ctlv_node_schema **stack,
+ unsigned int depth, const struct ctlv_node_schema *node,
+ const struct ctlvctl_spec *spec)
+{
+ const struct ctlvctl_field_name *name = field_name(spec, node);
+ const char *text;
+ struct json_object *value;
+ __u64 number;
+
+ switch (node->kind) {
+ case CTLV_SCHEMA_UINT:
+ number = node->present & CTLV_NODE_SCHEMA_HAS_MIN ? node->min : 0;
+ value = json_object_new_uint64(number);
+ break;
+ case CTLV_SCHEMA_SINT:
+ number = node->present & CTLV_NODE_SCHEMA_HAS_MIN ? node->min : 0;
+ value = json_object_new_int64((__s64)number);
+ break;
+ case CTLV_SCHEMA_BOOL:
+ value = json_object_new_boolean(false);
+ break;
+ case CTLV_SCHEMA_STRING: {
+ size_t len = node->present & CTLV_NODE_SCHEMA_HAS_MIN_LENGTH ?
+ node->min_length : 0;
+ char *text = calloc(len + 1, 1);
+
+ if (!text)
+ return NULL;
+ memset(text, 'x', len);
+ value = json_object_new_string_len(text, len);
+ free(text);
+ break;
+ }
+ case CTLV_SCHEMA_BINARY: {
+ size_t len = node->present & CTLV_NODE_SCHEMA_HAS_MIN_LENGTH ?
+ node->min_length : 0;
+ unsigned char *bytes = calloc(len ? len : 1, 1);
+
+ if (!bytes)
+ return NULL;
+ value = json_binary(bytes, len);
+ free(bytes);
+ break;
+ }
+ case CTLV_SCHEMA_ENUM:
+ number = node->present & CTLV_NODE_SCHEMA_HAS_MIN ? node->min : 0;
+ text = ctlvctl_spec_value_name(name, number);
+ if (text)
+ value = json_object_new_string(text);
+ else
+ value = json_object_new_uint64(number);
+ break;
+ case CTLV_SCHEMA_FLAGS:
+ value = json_object_new_array();
+ break;
+ case CTLV_SCHEMA_RECORD:
+ if (depth + 1 >= PATH_DEPTH)
+ return NULL;
+ stack[depth + 1] = node;
+ value = json_template_object(stack, depth + 1, spec);
+ break;
+ case CTLV_SCHEMA_BLOB:
+ value = json_object_new_object();
+ if (value)
+ json_add_string(value, "file", "");
+ break;
+ default:
+ fprintf(stderr, "%s: cannot template schema kind %u\n", prog,
+ node->kind);
+ return NULL;
+ }
+ if (!value)
+ return NULL;
+ if (node->max_occurrences == 1)
+ return value;
+
+ {
+ struct json_object *array = json_object_new_array();
+
+ if (!array) {
+ json_object_put(value);
+ return NULL;
+ }
+ json_object_array_add(array, value);
+ return array;
+ }
+}
+
+static struct json_object *
+json_template_object(const struct ctlv_node_schema **stack,
+ unsigned int depth, const struct ctlvctl_spec *spec)
+{
+ const struct ctlv_node_schema *parent = node_members(stack, depth);
+ struct json_object *object;
+ unsigned int i;
+
+ if (!parent)
+ return NULL;
+ object = json_object_new_object();
+ if (!object)
+ return NULL;
+ for (i = 0; i < parent->fields_count; i++) {
+ const struct ctlv_node_schema *node = &parent->fields[i];
+ const struct ctlvctl_field_name *name = field_name(spec, node);
+ struct json_object *value;
+ char numeric[32];
+ const char *key;
+
+ value = json_template_value(stack, depth, node, spec);
+ if (!value) {
+ json_object_put(object);
+ return NULL;
+ }
+ if (name) {
+ key = name->name;
+ } else {
+ snprintf(numeric, sizeof(numeric), "%#018llx",
+ (unsigned long long)node->type);
+ key = numeric;
+ }
+ json_object_object_add(object, key, value);
+ }
+ return object;
+}
+
+static int json_template_print(const struct ctlv_node_schema *root,
+ const struct ctlvctl_spec *spec)
+{
+ const struct ctlv_node_schema *stack[PATH_DEPTH] = { root };
+
+ return json_print(json_template_object(stack, 0, spec));
+}
+
+#endif /* HAVE_JSON_C */
+
+static void payload_print(const struct ctlv_hdr *hdr,
+ const struct ctlv_node_schema *node,
+ const struct ctlvctl_spec *spec)
+{
+ const struct ctlvctl_field_name *name = field_name(spec, node);
+ const void *bytes;
+ const char *text;
+ __u64 value;
+ __s64 signed_value;
+ bool flag;
+ __u32 len, i;
+
+ switch (node ? node->kind : CTLV_SCHEMA_BINARY) {
+ case CTLV_SCHEMA_UINT:
+ if (!ctlv_get_u64(hdr, &value))
+ printf("%llu", (unsigned long long)value);
+ return;
+ case CTLV_SCHEMA_ENUM:
+ if (ctlv_get_u64(hdr, &value))
+ return;
+ text = ctlvctl_spec_value_name(name, value);
+ if (!text) {
+ printf("%llu", (unsigned long long)value);
+ return;
+ }
+ printf("%s", text);
+ if (options.show_ids)
+ printf(" (%llu)", (unsigned long long)value);
+ return;
+ case CTLV_SCHEMA_FLAGS:
+ if (ctlv_get_u64(hdr, &value))
+ return;
+ if (name && name->definition) {
+ __u64 unknown = value;
+ bool first = true;
+
+ for (i = 0; i < name->definition->values_count; i++) {
+ const struct ctlvctl_value_name *item =
+ &name->definition->values[i];
+
+ if (!(value & item->value))
+ continue;
+ printf("%s%s", first ? "" : ",", item->name);
+ unknown &= ~item->value;
+ first = false;
+ }
+ if (unknown || first)
+ printf("%s%#llx", first ? "" : ",",
+ (unsigned long long)unknown);
+ if (options.show_ids)
+ printf(" (%#llx)", (unsigned long long)value);
+ } else {
+ printf("%#llx", (unsigned long long)value);
+ }
+ return;
+ case CTLV_SCHEMA_SINT:
+ if (!ctlv_get_s64(hdr, &signed_value))
+ printf("%lld", (long long)signed_value);
+ return;
+ case CTLV_SCHEMA_BOOL:
+ if (!ctlv_get_bool(hdr, &flag))
+ printf("%s", flag ? "true" : "false");
+ return;
+ case CTLV_SCHEMA_STRING:
+ if (!ctlv_get_str(hdr, &text))
+ printf("%s", text);
+ return;
+ default:
+ if (ctlv_get_bin(hdr, &bytes, &len))
+ return;
+ if (name)
+ printf("hex:");
+ for (i = 0; i < len; i++)
+ printf("%02x", ((const __u8 *)bytes)[i]);
+ return;
+ }
+}
+
+/*
+ * Printed as the schema says, and a TLV the schema does not mention is
+ * printed as bytes with a "?": a device newer than this tool is not an error.
+ */
+static void reply_print(const struct ctlv_hdr *record,
+ const struct ctlv_node_schema **stack,
+ unsigned int depth, const struct ctlvctl_spec *spec)
+{
+ const struct ctlv_hdr *child;
+
+ ctlv_for_each(child, record) {
+ const struct ctlv_node_schema *node = NULL;
+ const struct ctlvctl_field_name *name;
+
+ if (depth < PATH_DEPTH)
+ node = node_member(stack, depth, child->type);
+ name = node ? field_name(spec, node) : NULL;
+ printf("%*s", (depth + 1) * 2, "");
+ if (name) {
+ printf("%s", name->name);
+ if (options.show_ids)
+ printf(" (%#018llx)",
+ (unsigned long long)child->type);
+ } else {
+ printf("%#018llx%s",
+ (unsigned long long)child->type,
+ node ? "" : "?");
+ }
+ if (ctlv_hdr_is_record(child)) {
+ printf("\n");
+ if (depth + 1 < PATH_DEPTH) {
+ stack[depth + 1] = node;
+ reply_print(child, stack, depth + 1, spec);
+ }
+ continue;
+ }
+ printf(" ");
+ payload_print(child, node, spec);
+ printf("\n");
+ }
+}
+
+static void reply_dump(const struct ctlv_hdr *root,
+ const struct ctlv_node_schema *tree,
+ const struct ctlvctl_spec *spec)
+{
+ const struct ctlv_node_schema *stack[PATH_DEPTH] = { tree };
+
+ reply_print(root, stack, 0, spec);
+}
+
+static int run_op_info(const struct ctlv_dev *dev,
+ const struct ctlvctl_spec *spec,
+ const struct ctlv_ops *ops, __u64 op_id,
+ const struct ctlv_op_info **infop)
+{
+ const struct ctlvctl_op_name *name = ctlvctl_spec_op_id(spec, op_id);
+ const struct ctlv_op_info *info = ctlv_ops_find(ops, op_id);
+
+ if (!info) {
+ fprintf(stderr, "%s: operation is not available on %s\n",
+ prog, ctlv_dev_name(dev));
+ return -ENOENT;
+ }
+ if (name && !op_matches(name, info)) {
+ fprintf(stderr, "%s: specification disagrees with operation %s\n",
+ prog, name->name);
+ return -ENOMSG;
+ }
+ if (info->fd_access != CTLV_OP_FD_ACCESS_READ &&
+ info->fd_access != CTLV_OP_FD_ACCESS_READ_WRITE)
+ return -EPROTO;
+ *infop = info;
+ return 0;
+}
+
+static int cmd_run(int argc, char **argv)
+{
+ const struct ctlvctl_op_name *name;
+ const struct ctlv_node_schema *request_tree;
+ const struct ctlv_op_info *info;
+ struct ctlvctl_spec *spec = NULL;
+ struct ctlv_msg *request = NULL;
+ struct ctlv_reply *reply = NULL;
+ struct blob_files blobs = {};
+ struct ctlv_schema *schema;
+ struct ctlv_ops *ops = NULL;
+ struct ctlv_error error;
+ struct ctlv_dev *dev;
+ const char *json_source = NULL;
+ char *json_text = NULL;
+ char text[LINE_LEN];
+ size_t json_len = 0;
+ __u64 reply_len = 0;
+ __u64 op_id;
+ __u32 fd_access;
+ __u32 type;
+ int ret;
+
+ if (argc > 1 && !strcmp(argv[0], "-l")) {
+ ret = parse_u64(argv[1], &reply_len);
+ if (ret || !reply_len || reply_len > UINT32_MAX) {
+ fprintf(stderr, "%s: %s: not a reply length\n", prog,
+ argv[1]);
+ return ret ? ret : -EINVAL;
+ }
+ argc -= 2;
+ argv += 2;
+ }
+
+ if (argc < 2) {
+ help(stderr);
+ return -EINVAL;
+ }
+ if (argc > 2 && !strcmp(argv[2], "--json")) {
+ if (argc != 4) {
+ fprintf(stderr,
+ "%s: --json needs one source and no assignments\n",
+ prog);
+ return -EINVAL;
+ }
+ json_source = argv[3];
+ }
+ /* Discovery is always available through an observation fd. */
+ ret = dev_open(argv[0], O_RDONLY, &dev);
+ if (ret)
+ return ret;
+ ret = spec_load(dev, &spec);
+ if (ret)
+ goto out_close;
+ ret = op_resolve(spec, argv[1], &op_id);
+ if (ret)
+ goto out_close;
+
+ name = ctlvctl_spec_op_id(spec, op_id);
+ ret = ctlv_ops_get(dev, &ops, &error);
+ if (ret) {
+ fprintf(stderr, "%s: op-get: %s\n", prog,
+ ctlv_error_str(&error, text, sizeof(text)));
+ goto out_close;
+ }
+ ret = run_op_info(dev, spec, ops, op_id, &info);
+ if (ret)
+ goto out_free_ops;
+ fd_access = info->fd_access;
+ type = info->type;
+ ctlv_ops_free(ops);
+ ops = NULL;
+
+ if (fd_access == CTLV_OP_FD_ACCESS_READ_WRITE) {
+ __u64 reopened_id;
+
+ ctlvctl_spec_free(spec);
+ spec = NULL;
+ ctlv_close(dev);
+ dev = NULL;
+ ret = dev_open(argv[0], O_RDWR, &dev);
+ if (ret)
+ return ret;
+ ret = spec_load(dev, &spec);
+ if (ret)
+ goto out_close;
+ ret = op_resolve(spec, argv[1], &reopened_id);
+ if (ret)
+ goto out_close;
+ if (reopened_id != op_id) {
+ ret = -ENODEV;
+ goto out_close;
+ }
+ name = ctlvctl_spec_op_id(spec, op_id);
+ ret = ctlv_ops_get(dev, &ops, &error);
+ if (ret) {
+ fprintf(stderr, "%s: op-get: %s\n", prog,
+ ctlv_error_str(&error, text, sizeof(text)));
+ goto out_close;
+ }
+ ret = run_op_info(dev, spec, ops, op_id, &info);
+ if (ret)
+ goto out_free_ops;
+ if (info->fd_access != fd_access || info->type != type) {
+ ret = -ENODEV;
+ goto out_free_ops;
+ }
+ ctlv_ops_free(ops);
+ ops = NULL;
+ }
+
+ /* The schema first: it says how to read values and print the reply. */
+ ret = ctlv_op_schema_get(dev, op_id, &schema, &error);
+ if (ret) {
+ fprintf(stderr, "%s: op-schema-get: %s\n", prog,
+ ctlv_error_str(&error, text, sizeof(text)));
+ goto out_close;
+ }
+ ret = tree_names_check(spec, ctlv_schema_request(schema));
+ if (!ret)
+ ret = tree_names_check(spec, ctlv_schema_reply(schema));
+ if (ret)
+ goto out_free_schema;
+
+ request_tree = ctlv_schema_request(schema);
+ if ((argc > 2 || json_source) && !request_tree) {
+ fprintf(stderr, "%s: this op takes no request\n", prog);
+ ret = -EINVAL;
+ goto out_free_schema;
+ }
+ if (request_tree) {
+ ret = ctlv_msg_new(REQUEST_HINT, &request);
+ if (ret)
+ goto out_free_schema;
+ if (json_source) {
+ ret = json_source_read(json_source, &json_text,
+ &json_len);
+ if (!ret)
+ ret = json_request_build(request, request_tree,
+ spec, json_text,
+ json_len, &blobs);
+ } else {
+ ret = request_build(request, request_tree, spec,
+ argc - 2, argv + 2);
+ }
+ if (ret)
+ goto out_free_request;
+ }
+
+ for (;;) {
+ if (type == CTLV_OP_TYPE_QUERY && reply_len)
+ ret = ctlv_query_sized(dev, op_id, request, reply_len,
+ &reply, &error);
+ else if (type == CTLV_OP_TYPE_QUERY)
+ ret = ctlv_query(dev, op_id, request, &reply, &error);
+ else if (reply_len)
+ ret = ctlv_action_sized(dev, op_id, request, reply_len,
+ &reply, &error);
+ else
+ ret = ctlv_action(dev, op_id, request, &reply, &error);
+ if (!ret || !blobs_grow(&blobs, &error))
+ break;
+ ret = json_request_build(request, request_tree, spec,
+ json_text, json_len, &blobs);
+ if (ret)
+ goto out_free_request;
+ }
+ if (ret) {
+ fprintf(stderr, "%s: %s\n", prog,
+ ctlv_error_str(&error, text, sizeof(text)));
+ goto out_free_request;
+ }
+ ret = blobs_save(&blobs);
+ if (ret)
+ goto out_free_reply;
+
+ if (options.output_json) {
+#ifdef HAVE_JSON_C
+ struct json_object *output = json_object_new_object();
+
+ if (!output) {
+ ret = -ENOMEM;
+ goto out_free_reply;
+ }
+ json_add_string(output, "device", ctlv_dev_name(dev));
+ json_add_op(output, spec, op_id);
+ if (!reply) {
+ json_add_bool(output, "acknowledged", true);
+ } else {
+ struct json_object *body;
+
+ json_add_u64(output, "generation",
+ ctlv_reply_generation(reply));
+ json_add_int(output, "fragments",
+ ctlv_reply_fragments(reply));
+ body = json_reply(ctlv_reply_root(reply),
+ ctlv_schema_reply(schema), spec);
+ if (!body) {
+ json_object_put(output);
+ ret = -ENOMEM;
+ goto out_free_reply;
+ }
+ json_object_object_add(output, "reply", body);
+ }
+ ret = json_print(output);
+#endif
+ } else if (!reply) {
+ printf("# acknowledged\n");
+ } else {
+ printf("# ");
+ if (name) {
+ printf("%s", name->name);
+ if (options.show_ids)
+ printf(" (%#018llx)",
+ (unsigned long long)op_id);
+ printf(", ");
+ }
+ printf("%u fragment%s, generation %llu\n",
+ ctlv_reply_fragments(reply),
+ ctlv_reply_fragments(reply) == 1 ? "" : "s",
+ (unsigned long long)ctlv_reply_generation(reply));
+ reply_dump(ctlv_reply_root(reply), ctlv_schema_reply(schema),
+ spec);
+ }
+out_free_reply:
+ ctlv_reply_free(reply);
+out_free_request:
+ ctlv_msg_free(request);
+ blobs_free(&blobs);
+ free(json_text);
+out_free_schema:
+ ctlv_schema_free(schema);
+out_close:
+ ctlvctl_spec_free(spec);
+ ctlv_close(dev);
+ return ret;
+out_free_ops:
+ ctlv_ops_free(ops);
+ goto out_close;
+}
+
+int main(int argc, char **argv)
+{
+ static const struct option long_options[] = {
+ { "spec-dir", required_argument, NULL, 1 },
+ { "show-ids", no_argument, NULL, 'n' },
+ { "output-json", no_argument, NULL, 2 },
+ { "help", no_argument, NULL, 'h' },
+ {},
+ };
+ const char *command;
+ const char *spec_dir;
+ int opt;
+
+ if (argc > 0)
+ prog = argv[0];
+ while ((opt = getopt_long(argc, argv, "+nh", long_options, NULL)) != -1) {
+ switch (opt) {
+ case 1:
+ options.spec_dir = optarg;
+ break;
+ case 2:
+ options.output_json = true;
+ break;
+ case 'n':
+ options.show_ids = true;
+ break;
+ case 'h':
+ help(stdout);
+ return EXIT_SUCCESS;
+ default:
+ help(stderr);
+ return EXIT_FAILURE;
+ }
+ }
+ if (!options.spec_dir) {
+ spec_dir = getenv("CTLV_SPEC_DIR");
+ if (spec_dir && *spec_dir)
+ options.spec_dir = spec_dir;
+ }
+#ifndef HAVE_JSON_C
+ if (options.spec_dir || options.output_json) {
+ fprintf(stderr,
+ "%s: specification and JSON support require json-c\n",
+ prog);
+ return EXIT_FAILURE;
+ }
+#endif
+ if (optind == argc) {
+ help(stderr);
+ return EXIT_FAILURE;
+ }
+ command = argv[optind++];
+ argc -= optind;
+ argv += optind;
+
+ if (!strcmp(command, "list"))
+ return cmd_list(argc, argv) ? EXIT_FAILURE : EXIT_SUCCESS;
+ if (!strcmp(command, "ops"))
+ return cmd_ops(argc, argv) ? EXIT_FAILURE : EXIT_SUCCESS;
+ if (!strcmp(command, "schema"))
+ return cmd_schema(argc, argv) ? EXIT_FAILURE : EXIT_SUCCESS;
+ if (!strcmp(command, "exec"))
+ return cmd_run(argc, argv) ? EXIT_FAILURE : EXIT_SUCCESS;
+ if (!strcmp(command, "help") || !strcmp(command, "--help")) {
+ help(stdout);
+ return EXIT_SUCCESS;
+ }
+ fprintf(stderr, "%s: %s: no such command\n", prog, command);
+ help(stderr);
+ return EXIT_FAILURE;
+}
diff --git a/tools/ctlv/ctlvctl/spec.c b/tools/ctlv/ctlvctl/spec.c
new file mode 100644
index 000000000000..7fa1e3de2d33
--- /dev/null
+++ b/tools/ctlv/ctlvctl/spec.c
@@ -0,0 +1,672 @@
+// SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
+#include <ctype.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include <ctlv.h>
+
+#include "spec.h"
+
+#define ARRAY_SIZE(array) (sizeof(array) / sizeof((array)[0]))
+
+struct ctlvctl_spec {
+ struct ctlvctl_definition *definitions;
+ unsigned int definitions_count;
+ struct ctlvctl_field_name *fields;
+ unsigned int fields_count;
+ struct ctlvctl_op_name *ops;
+ unsigned int ops_count;
+};
+
+#ifdef HAVE_JSON_C
+
+#include <json.h>
+
+static int parse_u64(const char *text, __u64 *value)
+{
+ char *end;
+
+ if (!*text || *text == '-' || isspace((unsigned char)*text))
+ return -EINVAL;
+ errno = 0;
+ *value = strtoull(text, &end, 0);
+ if (errno || end == text || *end)
+ return -EINVAL;
+ return 0;
+}
+
+static int json_member(struct json_object *parent, const char *name,
+ enum json_type type, struct json_object **value)
+{
+ if (!json_object_object_get_ex(parent, name, value) ||
+ !json_object_is_type(*value, type))
+ return -EINVAL;
+ return 0;
+}
+
+static int json_string_dup(struct json_object *parent, const char *name,
+ char **value)
+{
+ struct json_object *item;
+
+ if (json_member(parent, name, json_type_string, &item))
+ return -EINVAL;
+ *value = strdup(json_object_get_string(item));
+ return *value ? 0 : -ENOMEM;
+}
+
+static int json_u64(struct json_object *parent, const char *name, __u64 *value)
+{
+ struct json_object *item;
+
+ if (!json_object_object_get_ex(parent, name, &item))
+ return -EINVAL;
+ if (json_object_is_type(item, json_type_string))
+ return parse_u64(json_object_get_string(item), value);
+ if (!json_object_is_type(item, json_type_int))
+ return -EINVAL;
+ *value = json_object_get_uint64(item);
+ return 0;
+}
+
+static int kind_parse(const char *name, __u32 *kind)
+{
+ static const char *const names[] = {
+ [CTLV_SCHEMA_UINT] = "uint",
+ [CTLV_SCHEMA_SINT] = "sint",
+ [CTLV_SCHEMA_BOOL] = "bool",
+ [CTLV_SCHEMA_STRING] = "string",
+ [CTLV_SCHEMA_BINARY] = "binary",
+ [CTLV_SCHEMA_ENUM] = "enum",
+ [CTLV_SCHEMA_FLAGS] = "flags",
+ [CTLV_SCHEMA_RECORD] = "record",
+ [CTLV_SCHEMA_BLOB] = "blob",
+ };
+ unsigned int i;
+
+ for (i = 0; i < ARRAY_SIZE(names); i++)
+ if (names[i] && !strcmp(names[i], name)) {
+ *kind = i;
+ return 0;
+ }
+ return -EINVAL;
+}
+
+static int op_type_parse(const char *name, __u32 *type)
+{
+ if (!strcmp(name, "action"))
+ *type = CTLV_OP_TYPE_ACTION;
+ else if (!strcmp(name, "query"))
+ *type = CTLV_OP_TYPE_QUERY;
+ else
+ return -EINVAL;
+ return 0;
+}
+
+static int availability_parse(const char *name, __u32 *availability)
+{
+ if (!strcmp(name, "fixed"))
+ *availability = CTLV_OP_AVAILABILITY_FIXED;
+ else if (!strcmp(name, "dynamic"))
+ *availability = CTLV_OP_AVAILABILITY_DYNAMIC;
+ else
+ return -EINVAL;
+ return 0;
+}
+
+static int fd_access_parse(const char *name, __u32 *fd_access)
+{
+ if (!strcmp(name, "read"))
+ *fd_access = CTLV_OP_FD_ACCESS_READ;
+ else if (!strcmp(name, "read-write"))
+ *fd_access = CTLV_OP_FD_ACCESS_READ_WRITE;
+ else
+ return -EINVAL;
+ return 0;
+}
+
+static struct ctlvctl_definition *
+definition_find(struct ctlvctl_spec *spec, const char *name)
+{
+ unsigned int i;
+
+ for (i = 0; i < spec->definitions_count; i++)
+ if (!strcmp(spec->definitions[i].name, name))
+ return &spec->definitions[i];
+ return NULL;
+}
+
+static int definition_parse(struct json_object *json,
+ struct ctlvctl_definition *definition)
+{
+ struct json_object *kind, *values;
+ size_t i, n;
+ int ret;
+
+ ret = json_string_dup(json, "name", &definition->name);
+ if (ret)
+ return ret;
+ if (json_member(json, "kind", json_type_string, &kind) ||
+ kind_parse(json_object_get_string(kind), &definition->kind))
+ return -EINVAL;
+ if (definition->kind != CTLV_SCHEMA_ENUM &&
+ definition->kind != CTLV_SCHEMA_FLAGS)
+ return -EINVAL;
+ if (json_member(json, "values", json_type_array, &values))
+ return -EINVAL;
+
+ n = json_object_array_length(values);
+ definition->values = calloc(n, sizeof(*definition->values));
+ if (n && !definition->values)
+ return -ENOMEM;
+ definition->values_count = n;
+ for (i = 0; i < n; i++) {
+ struct json_object *item = json_object_array_get_idx(values, i);
+
+ if (!item ||
+ json_string_dup(item, "name",
+ &definition->values[i].name) ||
+ json_u64(item, "value", &definition->values[i].value))
+ return -EINVAL;
+ }
+ return 0;
+}
+
+static int definitions_parse(struct json_object *root,
+ struct ctlvctl_spec *spec)
+{
+ struct json_object *items;
+ size_t i, n;
+ int ret;
+
+ if (json_member(root, "definitions", json_type_array, &items))
+ return -EINVAL;
+ n = json_object_array_length(items);
+ spec->definitions = calloc(n, sizeof(*spec->definitions));
+ if (n && !spec->definitions)
+ return -ENOMEM;
+ spec->definitions_count = n;
+ for (i = 0; i < n; i++) {
+ ret = definition_parse(json_object_array_get_idx(items, i),
+ &spec->definitions[i]);
+ if (ret)
+ return ret;
+ }
+ return 0;
+}
+
+static int field_parse(struct json_object *json, struct ctlvctl_spec *spec,
+ struct ctlvctl_field_name *field)
+{
+ struct json_object *item;
+ const char *definition;
+ int ret;
+
+ ret = json_string_dup(json, "name", &field->name);
+ if (ret)
+ return ret;
+ ret = json_string_dup(json, "short", &field->short_name);
+ if (ret)
+ return ret;
+ ret = json_u64(json, "type", &field->type);
+ if (ret)
+ return ret;
+ if (json_member(json, "kind", json_type_string, &item) ||
+ kind_parse(json_object_get_string(item), &field->kind))
+ return -EINVAL;
+
+ if (!json_object_object_get_ex(json, "definition", &item))
+ return 0;
+ if (!json_object_is_type(item, json_type_string))
+ return -EINVAL;
+ definition = json_object_get_string(item);
+ field->definition = definition_find(spec, definition);
+ return field->definition ? 0 : -EINVAL;
+}
+
+static int fields_parse(struct json_object *root, struct ctlvctl_spec *spec)
+{
+ struct json_object *items;
+ size_t i, n;
+ int ret;
+
+ if (json_member(root, "fields", json_type_array, &items))
+ return -EINVAL;
+ n = json_object_array_length(items);
+ spec->fields = calloc(n, sizeof(*spec->fields));
+ if (n && !spec->fields)
+ return -ENOMEM;
+ spec->fields_count = n;
+ for (i = 0; i < n; i++) {
+ ret = field_parse(json_object_array_get_idx(items, i),
+ spec, &spec->fields[i]);
+ if (ret)
+ return ret;
+ }
+ return 0;
+}
+
+static int op_parse(struct json_object *json, struct ctlvctl_op_name *op)
+{
+ struct json_object *item;
+ int ret;
+
+ ret = json_string_dup(json, "name", &op->name);
+ if (ret)
+ return ret;
+ ret = json_u64(json, "id", &op->id);
+ if (ret)
+ return ret;
+ if (json_member(json, "type", json_type_string, &item) ||
+ op_type_parse(json_object_get_string(item), &op->type))
+ return -EINVAL;
+ if (json_member(json, "fd-access", json_type_string, &item) ||
+ fd_access_parse(json_object_get_string(item), &op->fd_access) ||
+ json_member(json, "availability", json_type_string, &item) ||
+ availability_parse(json_object_get_string(item),
+ &op->availability))
+ return -EINVAL;
+ if (json_u64(json, "request-max-len", &op->request_max_len) ||
+ json_u64(json, "action-reply-max-len",
+ &op->action_reply_max_len) ||
+ json_u64(json, "query-reply-min-len",
+ &op->query_reply_min_len) ||
+ json_u64(json, "query-reply-suggested-len",
+ &op->query_reply_suggested_len))
+ return -EINVAL;
+ return 0;
+}
+
+static int ops_parse(struct json_object *root, struct ctlvctl_spec *spec)
+{
+ struct json_object *items;
+ size_t i, n;
+ int ret;
+
+ if (json_member(root, "ops", json_type_array, &items))
+ return -EINVAL;
+ n = json_object_array_length(items);
+ spec->ops = calloc(n, sizeof(*spec->ops));
+ if (n && !spec->ops)
+ return -ENOMEM;
+ spec->ops_count = n;
+ for (i = 0; i < n; i++) {
+ ret = op_parse(json_object_array_get_idx(items, i), &spec->ops[i]);
+ if (ret)
+ return ret;
+ }
+ return 0;
+}
+
+static int chain_check(struct json_object *root, const struct ctlv_dev *dev)
+{
+ struct json_object *family, *chain;
+ size_t i, n;
+
+ if (json_member(root, "family", json_type_string, &family) ||
+ strcmp(json_object_get_string(family), ctlv_dev_family(dev)))
+ return -ENOMSG;
+ if (json_member(root, "chain", json_type_array, &chain))
+ return -EINVAL;
+ n = json_object_array_length(chain);
+ if (n != ctlv_chain_len(dev))
+ return -ENOMSG;
+
+ for (i = 0; i < n; i++) {
+ struct json_object *item, *name;
+ __u64 id;
+
+ item = json_object_array_get_idx(chain, i);
+ if (!item ||
+ json_member(item, "name", json_type_string, &name) ||
+ json_u64(item, "id", &id))
+ return -EINVAL;
+ if (id != ctlv_chain_id(dev, i) ||
+ strcmp(json_object_get_string(name),
+ ctlv_chain_name(dev, i)))
+ return -ENOMSG;
+ }
+ return 0;
+}
+
+static int manifest_parse(const char *prog, const char *text,
+ const struct ctlv_dev *dev,
+ struct ctlvctl_spec **specp)
+{
+ struct json_tokener *tokener;
+ struct json_object *root;
+ struct ctlvctl_spec *spec;
+ enum json_tokener_error error;
+ size_t text_len;
+ __u64 version;
+ int ret;
+
+ if (!text)
+ return -EINVAL;
+ text_len = strlen(text);
+ if (text_len >= INT_MAX)
+ return -E2BIG;
+ tokener = json_tokener_new();
+ if (!tokener)
+ return -ENOMEM;
+ json_tokener_set_flags(tokener, JSON_TOKENER_STRICT |
+ JSON_TOKENER_VALIDATE_UTF8);
+ root = json_tokener_parse_ex(tokener, text, text_len + 1);
+ error = json_tokener_get_error(tokener);
+ json_tokener_free(tokener);
+ if (error != json_tokener_success || !root) {
+ fprintf(stderr, "%s: invalid ctlvgen manifest: %s\n", prog,
+ json_tokener_error_desc(error));
+ return -EINVAL;
+ }
+ if (!json_object_is_type(root, json_type_object) ||
+ json_u64(root, "version", &version) || version != 1) {
+ fprintf(stderr, "%s: unsupported ctlvgen manifest\n", prog);
+ ret = -EINVAL;
+ goto out_root;
+ }
+
+ spec = calloc(1, sizeof(*spec));
+ if (!spec) {
+ ret = -ENOMEM;
+ goto out_root;
+ }
+ ret = chain_check(root, dev);
+ if (!ret)
+ ret = definitions_parse(root, spec);
+ if (!ret)
+ ret = fields_parse(root, spec);
+ if (!ret)
+ ret = ops_parse(root, spec);
+ if (ret) {
+ if (ret == -ENOMSG)
+ fprintf(stderr,
+ "%s: specifications do not match device %s\n",
+ prog, ctlv_dev_name(dev));
+ else
+ fprintf(stderr, "%s: malformed ctlvgen manifest\n",
+ prog);
+ ctlvctl_spec_free(spec);
+ goto out_root;
+ }
+ *specp = spec;
+out_root:
+ json_object_put(root);
+ return ret;
+}
+
+static int read_all(int fd, char **textp)
+{
+ size_t used = 0, size = 4096;
+ char *text;
+
+ text = malloc(size);
+ if (!text)
+ return -ENOMEM;
+ for (;;) {
+ ssize_t n;
+
+ if (used == size - 1) {
+ char *grown;
+
+ size *= 2;
+ grown = realloc(text, size);
+ if (!grown) {
+ free(text);
+ return -ENOMEM;
+ }
+ text = grown;
+ }
+ n = read(fd, text + used, size - used - 1);
+ if (n < 0) {
+ if (errno == EINTR)
+ continue;
+ free(text);
+ return -errno;
+ }
+ if (!n)
+ break;
+ used += n;
+ }
+ text[used] = '\0';
+ *textp = text;
+ return 0;
+}
+
+static void generator_exec(const char *prog, const char *spec_path)
+{
+ const char *override = getenv("CTLVGEN");
+ char executable[PATH_MAX];
+ char candidate[PATH_MAX];
+ char *slash;
+ ssize_t n;
+
+ if (override)
+ execl(override, override, "--spec", spec_path,
+ "--runtime-json", NULL);
+
+ n = readlink("/proc/self/exe", executable, sizeof(executable) - 1);
+ if (n > 0) {
+ static const char suffix[] = "/../ctlvgen/cli.py";
+ size_t len;
+
+ executable[n] = '\0';
+ slash = strrchr(executable, '/');
+ if (slash) {
+ *slash = '\0';
+ len = strlen(executable);
+ if (len + sizeof(suffix) <= sizeof(candidate)) {
+ memcpy(candidate, executable, len);
+ memcpy(candidate + len, suffix, sizeof(suffix));
+ execl(candidate, candidate, "--spec",
+ spec_path, "--runtime-json", NULL);
+ }
+ }
+ }
+ execlp("ctlvgen", "ctlvgen", "--spec", spec_path,
+ "--runtime-json", NULL);
+ dprintf(STDERR_FILENO, "%s: cannot execute ctlvgen: %s\n", prog,
+ strerror(errno));
+ _exit(127);
+}
+
+static int manifest_generate(const char *prog, const char *spec_path,
+ char **textp)
+{
+ int pipefd[2];
+ int status;
+ pid_t pid;
+ int ret;
+
+ if (pipe2(pipefd, O_CLOEXEC))
+ return -errno;
+ pid = fork();
+ if (pid < 0) {
+ ret = -errno;
+ goto out_pipe;
+ }
+ if (!pid) {
+ close(pipefd[0]);
+ if (pipefd[1] == STDOUT_FILENO) {
+ int flags = fcntl(STDOUT_FILENO, F_GETFD);
+
+ if (flags < 0 ||
+ fcntl(STDOUT_FILENO, F_SETFD,
+ flags & ~FD_CLOEXEC) < 0)
+ _exit(127);
+ } else {
+ if (dup2(pipefd[1], STDOUT_FILENO) < 0)
+ _exit(127);
+ close(pipefd[1]);
+ }
+ generator_exec(prog, spec_path);
+ }
+ close(pipefd[1]);
+ ret = read_all(pipefd[0], textp);
+ close(pipefd[0]);
+ while (waitpid(pid, &status, 0) < 0) {
+ if (errno != EINTR) {
+ ret = -errno;
+ goto out_text;
+ }
+ }
+ if (!ret && (!WIFEXITED(status) || WEXITSTATUS(status)))
+ ret = -EINVAL;
+out_text:
+ if (ret) {
+ free(*textp);
+ *textp = NULL;
+ }
+ return ret;
+out_pipe:
+ close(pipefd[0]);
+ close(pipefd[1]);
+ return ret;
+}
+
+static int manifest_read(const char *path, char **textp)
+{
+ int fd, ret;
+
+ fd = open(path, O_RDONLY | O_CLOEXEC);
+ if (fd < 0)
+ return -errno;
+ ret = read_all(fd, textp);
+ close(fd);
+ return ret;
+}
+
+int ctlvctl_spec_load(const char *prog, const char *dir,
+ const struct ctlv_dev *dev, struct ctlvctl_spec **specp)
+{
+ char path[PATH_MAX];
+ char *text = NULL;
+ int ret;
+
+ if (snprintf(path, sizeof(path), "%s/%s.json", dir,
+ ctlv_dev_family(dev)) >= (int)sizeof(path))
+ return -ENAMETOOLONG;
+ ret = manifest_read(path, &text);
+ if (ret == -ENOENT) {
+ snprintf(path, sizeof(path), "%s/%s.yaml", dir,
+ ctlv_dev_family(dev));
+ ret = manifest_generate(prog, path, &text);
+ } else if (ret) {
+ fprintf(stderr, "%s: %s: %s\n", prog, path, strerror(-ret));
+ }
+ if (ret)
+ return ret;
+ ret = manifest_parse(prog, text, dev, specp);
+ free(text);
+ return ret;
+}
+
+#else /* HAVE_JSON_C */
+
+int ctlvctl_spec_load(const char *prog, const char *dir,
+ const struct ctlv_dev *dev, struct ctlvctl_spec **specp)
+{
+ fprintf(stderr,
+ "%s: specification support was built without json-c\n", prog);
+ return -EOPNOTSUPP;
+}
+
+#endif /* HAVE_JSON_C */
+
+void ctlvctl_spec_free(struct ctlvctl_spec *spec)
+{
+ unsigned int i, j;
+
+ if (!spec)
+ return;
+ for (i = 0; i < spec->definitions_count; i++) {
+ for (j = 0; j < spec->definitions[i].values_count; j++)
+ free(spec->definitions[i].values[j].name);
+ free(spec->definitions[i].values);
+ free(spec->definitions[i].name);
+ }
+ for (i = 0; i < spec->fields_count; i++) {
+ free(spec->fields[i].name);
+ free(spec->fields[i].short_name);
+ }
+ for (i = 0; i < spec->ops_count; i++)
+ free(spec->ops[i].name);
+ free(spec->definitions);
+ free(spec->fields);
+ free(spec->ops);
+ free(spec);
+}
+
+const struct ctlvctl_op_name *
+ctlvctl_spec_op_id(const struct ctlvctl_spec *spec, __u64 id)
+{
+ unsigned int i;
+
+ if (!spec)
+ return NULL;
+ for (i = 0; i < spec->ops_count; i++)
+ if (spec->ops[i].id == id)
+ return &spec->ops[i];
+ return NULL;
+}
+
+const struct ctlvctl_op_name *
+ctlvctl_spec_op_name(const struct ctlvctl_spec *spec, const char *name)
+{
+ unsigned int i;
+
+ if (!spec)
+ return NULL;
+ for (i = 0; i < spec->ops_count; i++)
+ if (!strcmp(spec->ops[i].name, name))
+ return &spec->ops[i];
+ return NULL;
+}
+
+const struct ctlvctl_field_name *
+ctlvctl_spec_field(const struct ctlvctl_spec *spec, __u64 type)
+{
+ unsigned int i;
+
+ if (!spec)
+ return NULL;
+ for (i = 0; i < spec->fields_count; i++)
+ if (spec->fields[i].type == type)
+ return &spec->fields[i];
+ return NULL;
+}
+
+const char *ctlvctl_spec_value_name(const struct ctlvctl_field_name *field,
+ __u64 value)
+{
+ unsigned int i;
+
+ if (!field || !field->definition)
+ return NULL;
+ for (i = 0; i < field->definition->values_count; i++)
+ if (field->definition->values[i].value == value)
+ return field->definition->values[i].name;
+ return NULL;
+}
+
+int ctlvctl_spec_value(const struct ctlvctl_field_name *field, const char *name,
+ __u64 *value)
+{
+ unsigned int i;
+
+ if (!field || !field->definition)
+ return -ENOENT;
+ for (i = 0; i < field->definition->values_count; i++)
+ if (!strcmp(field->definition->values[i].name, name)) {
+ *value = field->definition->values[i].value;
+ return 0;
+ }
+ return -ENOENT;
+}
diff --git a/tools/ctlv/ctlvctl/spec.h b/tools/ctlv/ctlvctl/spec.h
new file mode 100644
index 000000000000..0ac6e314232e
--- /dev/null
+++ b/tools/ctlv/ctlvctl/spec.h
@@ -0,0 +1,60 @@
+/* SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause */
+#ifndef CTLVCTL_SPEC_H
+#define CTLVCTL_SPEC_H
+
+#include <linux/types.h>
+#include <stdbool.h>
+
+struct ctlv_dev;
+
+struct ctlvctl_value_name {
+ __u64 value;
+ char *name;
+};
+
+struct ctlvctl_definition {
+ char *name;
+ __u32 kind;
+ struct ctlvctl_value_name *values;
+ unsigned int values_count;
+};
+
+struct ctlvctl_field_name {
+ __u64 type;
+ __u32 kind;
+ char *name;
+ char *short_name;
+ const struct ctlvctl_definition *definition;
+};
+
+struct ctlvctl_op_name {
+ __u64 id;
+ __u32 type;
+ __u32 fd_access;
+ __u32 availability;
+ __u64 request_max_len;
+ __u64 action_reply_max_len;
+ __u64 query_reply_min_len;
+ __u64 query_reply_suggested_len;
+ char *name;
+};
+
+struct ctlvctl_spec;
+
+int ctlvctl_spec_load(const char *prog, const char *dir,
+ const struct ctlv_dev *dev, struct ctlvctl_spec **specp);
+void ctlvctl_spec_free(struct ctlvctl_spec *spec);
+
+const struct ctlvctl_op_name *
+ctlvctl_spec_op_id(const struct ctlvctl_spec *spec, __u64 id);
+const struct ctlvctl_op_name *
+ctlvctl_spec_op_name(const struct ctlvctl_spec *spec, const char *name);
+const struct ctlvctl_field_name *
+ctlvctl_spec_field(const struct ctlvctl_spec *spec, __u64 type);
+
+const char *ctlvctl_spec_value_name(const struct ctlvctl_field_name *field,
+ __u64 value);
+int ctlvctl_spec_value(const struct ctlvctl_field_name *field, const char *name,
+ __u64 *value);
+
+#endif /* CTLVCTL_SPEC_H */
diff --git a/tools/ctlv/ctlvgen/cli.py b/tools/ctlv/ctlvgen/cli.py
index 59180f9757e8..5da444984bd5 100755
--- a/tools/ctlv/ctlvgen/cli.py
+++ b/tools/ctlv/ctlvgen/cli.py
@@ -14,7 +14,10 @@ from collections.abc import Callable
sys.path.append(pathlib.Path(__file__).resolve().parents[1].as_posix())
from ctlvgen.lib import cgen # noqa: E402
+from ctlvgen.lib import dump # noqa: E402
+from ctlvgen.lib import gen_doc # noqa: E402
from ctlvgen.lib import gen_kernel # noqa: E402
+from ctlvgen.lib import gen_runtime # noqa: E402
from ctlvgen.lib import gen_uapi # noqa: E402
from ctlvgen.lib import gen_user # noqa: E402
from ctlvgen.lib import semantics # noqa: E402
@@ -30,6 +33,7 @@ GENERATORS: dict[tuple[str, str], Callable[..., str]] = {
('kernel', 'source'): gen_kernel.render_source,
('user', 'header'): gen_user.render_header,
('user', 'source'): gen_user.render_source,
+ ('doc', 'rst'): gen_doc.render,
}
@@ -46,6 +50,12 @@ def main() -> int:
action = parser.add_mutually_exclusive_group(required=True)
action.add_argument('--validate', action='store_true',
help='validate the specification and exit')
+ action.add_argument('--dump', action='store_true',
+ help='print the resolved schema in text form')
+ action.add_argument('--runtime-json', action='store_true',
+ help='print the resolved runtime manifest as JSON')
+ action.add_argument('--signatures', action='store_true',
+ help='print each op the family declares as a call')
action.add_argument('--outputs-check', action='store_true',
help='check every file in the output manifests and '
'every ABI snapshot')
@@ -53,7 +63,7 @@ def main() -> int:
help='regenerate every file in the output manifests '
'and every ABI snapshot')
action.add_argument('--mode',
- choices=('uapi', 'kernel', 'user'),
+ choices=('uapi', 'kernel', 'user', 'doc'),
help='generate the files of this mode')
parser.add_argument('--schema-only', action='store_true',
help='omit the family API from kernel output')
@@ -62,6 +72,8 @@ def main() -> int:
help='generate the header of --mode')
files.add_argument('--source', action='store_true',
help='generate the source of --mode')
+ files.add_argument('--rst', action='store_true',
+ help='generate the reStructuredText of --mode')
parser.add_argument('-o', '--output', type=str,
help='write to this file instead of standard output')
parser.add_argument('--cmp-out', action='store_true',
@@ -73,12 +85,12 @@ def main() -> int:
help='let --outputs-update break the committed ABI')
args = parser.parse_args()
- file = next((name for name in ('header', 'source')
+ file = next((name for name in ('header', 'source', 'rst')
if getattr(args, name)), None)
if args.mode and not file:
- parser.error('--mode needs --header or --source')
+ parser.error('--mode needs --header, --source or --rst')
if file and not args.mode:
- parser.error('--header and --source need --mode')
+ parser.error('--header, --source and --rst need --mode')
if args.mode and (args.mode, file) not in GENERATORS:
parser.error(f'--mode {args.mode} cannot generate --{file}')
if args.schema_only and args.mode != 'kernel':
@@ -111,20 +123,43 @@ def main() -> int:
# The root family has no binding: libctlv is that binding.
if family.name != spec.CORE_FAMILY_NAME:
cgen.user_symbols(family)
+ elif args.dump:
+ sys.stdout.write(dump.render(family))
+ elif args.runtime_json:
+ sys.stdout.write(gen_runtime.render(family))
+ elif args.signatures:
+ sys.stdout.write(gen_doc.signatures(family))
else:
assert file is not None
- _generate(args, file, family)
+ _generate(args, paths, file, family)
except CtlvError as exc:
print(f'ctlvgen: {exc}', file=sys.stderr)
return 1
return 0
-def _generate(args: argparse.Namespace, file: str,
+def _descendants(args: argparse.Namespace, paths: spec.Paths,
+ family: semantics.Family) -> tuple[semantics.Family, ...]:
+ """Every family of the registry that inherits family."""
+ found = []
+ for entry in spec.load_registry(paths).entries:
+ path = paths.spec_dir / f'{entry.name}.yaml'
+ if entry.name == family.name or not path.exists():
+ continue
+ child = semantics.load(spec.Paths(str(path), schema=args.schema,
+ registry=args.registry))
+ if family.name in (item.name for item in child.chain[:-1]):
+ found.append(child)
+ return tuple(found)
+
+
+def _generate(args: argparse.Namespace, paths: spec.Paths, file: str,
family: semantics.Family) -> None:
render = GENERATORS[(args.mode, file)]
if args.mode == 'kernel':
text = render(family, family_api=not args.schema_only)
+ elif args.mode == 'doc':
+ text = render(family, _descendants(args, paths, family))
else:
text = render(family)
if args.output is None:
diff --git a/tools/ctlv/ctlvgen/lib/dump.py b/tools/ctlv/ctlvgen/lib/dump.py
new file mode 100644
index 000000000000..20eca794aa11
--- /dev/null
+++ b/tools/ctlv/ctlvgen/lib/dump.py
@@ -0,0 +1,161 @@
+# SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
+"""Deterministic text rendering of an effective family.
+
+A debugging window and a golden-test surface, not an interchange format: every
+generated ID, effective bound and computed length, in declaration order.
+"""
+
+from __future__ import annotations
+
+from . import semantics
+from . import sizes
+from . import spec
+
+INDENT: str = ' '
+
+
+def render(family: semantics.Family) -> str:
+ """Return the dump of one effective family as a string."""
+ lines = []
+ lines.append(f'family: {family.name}')
+ lines.append(f'family-id: {family.family_id}')
+ lines.append('chain: ' + ' -> '.join(item.name for item in family.chain))
+
+ _definitions(lines, family)
+ _records(lines, family)
+ _extensions(lines, family)
+ _ops(lines, family)
+ return '\n'.join(lines) + '\n'
+
+
+def _definitions(lines: list[str], family: semantics.Family) -> None:
+ if not family.definitions:
+ return
+ lines.append('')
+ lines.append('definitions:')
+ for definition in family.definitions.values():
+ extension = definition.extension
+ what = ''
+ if extension is not None:
+ what = f', extension {extension.ext_id} of family ' \
+ f'{extension.family_id}'
+ lines.append(f'{INDENT}{definition.kind} {definition.name} '
+ f'(family {definition.family_id}{what})')
+ for name, number in definition.values:
+ if definition.kind == 'enum':
+ lines.append(f'{INDENT * 2}{name} = {number}')
+ else:
+ lines.append(f'{INDENT * 2}{name} = bit {number}')
+
+
+def _records(lines: list[str], family: semantics.Family) -> None:
+ if not family.records:
+ return
+ lines.append('')
+ lines.append('records:')
+ for record in family.records.values():
+ lines.append(f'{INDENT}{record.path} = {record.record_id:#010x} '
+ f'{record.role} (family {record.family_id})')
+ for member in record.members:
+ field = member.field
+ requirement = '' if member.required else '?'
+ lines.append(f'{INDENT * 2}{field.name}{requirement} = '
+ f'{_id(field.type)} {_field_detail(field)}')
+
+
+def _field_detail(field: semantics.Field) -> str:
+ decl = field.decl
+ detail = [decl.kind]
+ if decl.kind in ('uint', 'sint'):
+ detail.append(f'[{decl.min}, {decl.max}]')
+ if decl.unit is not None:
+ detail.append(f'unit {decl.unit} scale {decl.scale}')
+ elif decl.kind in ('string', 'binary'):
+ detail.append(f'length [{decl.min_length}, {decl.max_length}]')
+ elif decl.kind in ('enum', 'flags'):
+ assert decl.ref is not None
+ detail.append(decl.ref)
+ elif decl.kind == 'blob':
+ assert decl.blob is not None
+ detail.append(decl.blob.direction)
+ detail.append('backings ' + ','.join(decl.blob.backings))
+ detail.append(f'max-length {decl.blob.max_length}')
+ detail.append(f'alignment {decl.blob.alignment}')
+ elif decl.extension is not None:
+ detail.append(f'extension {decl.extension}')
+ else:
+ detail.append(f'record {field.holds}')
+ detail.append(_occurrences(decl.max_occurrences))
+ return ' '.join(detail)
+
+
+def _extensions(lines: list[str], family: semantics.Family) -> None:
+ if not family.extensions:
+ return
+ lines.append('')
+ lines.append('extensions:')
+ for extension in family.extensions.values():
+ fill = family.fill_of(extension)
+ filled = f', filled by family {fill.owner_family_id}' if fill else ''
+ lines.append(f'{INDENT}{extension.name} = {extension.ext_id} '
+ f'(family {extension.family_id}{filled})')
+
+
+def _ops(lines: list[str], family: semantics.Family) -> None:
+ if not family.ops:
+ return
+ lines.append('')
+ lines.append('ops:')
+ for op in family.ops:
+ lines.append(f'{INDENT}{op.name} = {_id(op.op_id)} {op.type}')
+ lines.append(f'{INDENT * 2}fd-access: {op.fd_access}')
+ if op.request is not None:
+ _tree(lines, 'request', op.request)
+ if op.reply is None:
+ lines.append(f'{INDENT * 2}reply: none')
+ else:
+ _tree(lines, 'reply', op.reply)
+ _sizes(lines, op.sizes)
+
+
+def _tree(lines: list[str], what: str, root: semantics.Node) -> None:
+ lines.append(f'{INDENT * 2}{what}:')
+ if not root.children():
+ lines.append(f'{INDENT * 3}(empty)')
+ return
+ _children(lines, root, 3)
+
+
+def _children(lines: list[str], node: semantics.Node, depth: int) -> None:
+ for child in node.children():
+ requirement = 'required' if child.required else 'optional'
+ line = (f'{INDENT * depth}{child.child_field.full_name} '
+ f'{_occurrences(child.max_occurrences)} {requirement}')
+ if child.reenters is not None:
+ line += f' -> {child.reenters}'
+ lines.append(line)
+ _children(lines, child, depth + 1)
+
+
+def _sizes(lines: list[str], op_sizes: sizes.OpSizes) -> None:
+ lines.append(f'{INDENT * 2}sizes:')
+ reported = (
+ ('request-max-len', op_sizes.request_max_len),
+ ('action-reply-max-len', op_sizes.action_reply_max_len),
+ ('query-reply-min-len', op_sizes.query_reply_min_len),
+ ('query-reply-suggested-len', op_sizes.query_reply_suggested_len),
+ ('error-info-max-len', op_sizes.error_info_max_len),
+ )
+ for name, value in reported:
+ if value:
+ lines.append(f'{INDENT * 3}{name}: {value}')
+
+
+def _occurrences(max_occurrences: int) -> str:
+ if max_occurrences == spec.UNLIMITED:
+ return 'x*'
+ return f'x{max_occurrences}'
+
+
+def _id(value: int) -> str:
+ return f'0x{value:016x}'
diff --git a/tools/ctlv/ctlvgen/lib/gen_doc.py b/tools/ctlv/ctlvgen/lib/gen_doc.py
new file mode 100644
index 000000000000..7d36d4a7dd9b
--- /dev/null
+++ b/tools/ctlv/ctlvgen/lib/gen_doc.py
@@ -0,0 +1,684 @@
+# SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
+"""The reference documentation of one family.
+
+Each op's request and reply as the tree the core admits, with every field's
+number, schema kind, bounds and doc, and what each descendant puts into an
+extension point shown where it lands. An inherited declaration is documented
+by the family that owns it.
+"""
+
+from __future__ import annotations
+
+import textwrap
+from typing import Union
+
+from . import cgen
+from . import semantics
+from . import spec
+
+# Documentation/doc-guide/sphinx.rst asks for lines that fit a terminal, which
+# is why a generated tree is an indented literal block.
+LINE_LIMIT: int = 79
+
+# A literal block's indent, the columns of one tree row inside it, and how far
+# in from its row a doc goes.
+BLOCK_INDENT: int = 4
+NAME_WIDTH: int = 30
+NUMBER_WIDTH: int = 4
+DOC_INDENT: int = 4
+
+TYPES: dict[str, str] = {
+ 'action': 'action',
+ 'query': 'query',
+}
+
+# A blob's direction as a spec writes it.
+DIRECTIONS: dict[str, str] = {value: key
+ for key, value in spec.DIRECTIONS.items()}
+
+
+def render(family: semantics.Family,
+ descendants: tuple[semantics.Family, ...] = ()) -> str:
+ """Return the reference page of family as reStructuredText.
+
+ descendants are the families inheriting this one, whose fills appear in
+ its trees.
+ """
+ writer = _Writer()
+ _preamble(writer, family)
+ _title(writer, family)
+ _intro(writer, family)
+ _definitions(writer, family)
+ _records(writer, family, descendants)
+ _extensions(writer, family, descendants)
+ _fills(writer, family, descendants)
+ _ops(writer, family, descendants)
+ _messages(writer, family, descendants)
+ return writer.text()
+
+
+def signatures(family: semantics.Family, width: int = 100) -> str:
+ """Return every op this family declares as one call per line."""
+ lines = []
+ for op in family.local_ops:
+ lines += _signature(family, op, width)
+ return '\n'.join(lines) + '\n' if lines else ''
+
+
+class _Writer:
+ """The lines of one generated page."""
+
+ def __init__(self) -> None:
+ self._lines: list[str] = []
+
+ def line(self, text: str = '') -> None:
+ self._lines.append(text.rstrip())
+
+ def lines(self, *texts: str) -> None:
+ for text in texts:
+ self.line(text)
+
+ def paragraph(self, text: str) -> None:
+ """One paragraph, wrapped, followed by a blank line.
+
+ A hyphenated family name is never broken, so a grep for one finds it.
+ """
+ for line in _wrap(text, LINE_LIMIT):
+ self.line(line)
+ self.line()
+
+ def title(self, text: str) -> None:
+ """The one title of the page, overlined and underlined."""
+ self.line('=' * len(text))
+ self.line(text)
+ self.line('=' * len(text))
+ self.line()
+
+ def heading(self, text: str, char: str) -> None:
+ """One section heading, underlined the width of its title."""
+ self.line(text)
+ self.line(char * len(text))
+ self.line()
+
+ def block(self, *texts: str) -> None:
+ """One literal block, indented, followed by a blank line."""
+ self.line('::')
+ self.line()
+ for text in texts:
+ self.line(f'{" " * BLOCK_INDENT}{text}' if text else '')
+ self.line()
+
+ def field(self, name: str, value: int | str) -> None:
+ self.line(f':{name}: {value}')
+
+ def text(self) -> str:
+ while self._lines and not self._lines[-1]:
+ self._lines.pop()
+ return '\n'.join(self._lines) + '\n'
+
+
+def _wrap(text: str, width: int) -> list[str]:
+ return textwrap.wrap(' '.join(text.split()), width,
+ break_on_hyphens=False, break_long_words=False)
+
+
+def _preamble(writer: _Writer, family: semantics.Family) -> None:
+ """The comment ctlv-regen.sh recovers the generation parameters from."""
+ writer.lines(
+ f'.. SPDX-License-Identifier: {family.license}',
+ f'.. Generated from {cgen.spec_path(family.path)} by',
+ f' {cgen.REGEN}. Do not edit.',
+ '.. CTLV-GEN doc rst',
+ )
+ writer.line()
+
+
+def _title(writer: _Writer, family: semantics.Family) -> None:
+ writer.title(family.name)
+
+
+def _intro(writer: _Writer, family: semantics.Family) -> None:
+ writer.field('Family ID', family.family_id)
+ writer.field('Chain', ' -> '.join(item.name for item in family.chain))
+ writer.field('Specification', cgen.spec_path(family.path))
+ writer.line()
+ if family.doc:
+ writer.paragraph(family.doc)
+ if family.parent:
+ writer.paragraph(
+ f'This family inherits ``{family.parent}`` and its page describes '
+ f'what ``{family.name}`` declares itself, together with the '
+ 'extension points it fills. An inherited op or field keeps the '
+ 'name and the description its own family gives it, so a program '
+ 'speaking to a device of this family uses both pages.')
+ else:
+ writer.paragraph(
+ 'This is the family every other family inherits: its ops are the '
+ 'ones the core answers itself, on every device, whatever '
+ 'family registered it.')
+ writer.paragraph(
+ 'A tree lists one field per line: its name as the specification '
+ 'writes it, its number and its schema kind. A field\'s type is the '
+ 'family, the record above it and that number. A field of records: '
+ 'is a record of its own, which its line names.')
+
+
+def _definitions(writer: _Writer, family: semantics.Family) -> None:
+ own = family.local_definitions
+ if not own:
+ return
+ writer.heading('Enumerations and flags', '=')
+ for definition in own:
+ writer.heading(definition.name, '-')
+ if definition.doc:
+ writer.paragraph(definition.doc)
+ if definition.is_extension_point:
+ writer.paragraph(
+ 'An enum extension point: the family of a device gives the '
+ 'values, by declaring an enum of this name. On a device '
+ 'whose family gives none, no value is valid.')
+ continue
+ if definition.extension is not None:
+ owner = family.owners[definition.extension.family_id]
+ writer.paragraph(f'The values this family gives the enum '
+ f'extension point of {owner}.')
+ if definition.kind == 'enum':
+ writer.paragraph(
+ 'A payload restricted to these values. An unknown value is '
+ 'refused on a request and tolerated on a reply, so a caller '
+ 'built against an older specification keeps working.')
+ header = definition.header
+ if header:
+ before = f' before ``{header.end}``' if header.end else ''
+ writer.paragraph(
+ f'These are the constants of ``enum {header.tag}`` in '
+ f'``<{header.path}>``{before}, under its names and '
+ 'numbers. A number between two of them passes CTLV, for '
+ 'the family to refuse.')
+ rows = _value_rows(definition, '')
+ else:
+ writer.paragraph(
+ 'A payload restricted to these bits. An unknown bit is '
+ 'refused on a request and tolerated on a reply.')
+ rows = _value_rows(definition, 'bit ')
+ writer.block(*rows)
+
+
+def _value_rows(definition: semantics.Definition, prefix: str) -> list[str]:
+ """One line per value, its doc below."""
+ rows = []
+ width = LINE_LIMIT - BLOCK_INDENT - DOC_INDENT
+ for (name, number), doc in zip(definition.values, definition.value_docs):
+ rows.append(f'{prefix}{number} = {name}')
+ if doc:
+ rows += [f'{" " * DOC_INDENT}{line}' for line in _wrap(doc, width)]
+ return rows
+
+
+def _records(writer: _Writer, family: semantics.Family,
+ descendants: tuple[semantics.Family, ...]) -> None:
+ """The named records and the fields of records: this family declares."""
+ own = [record for record in family.local_records
+ if record.role in ('named', 'field')]
+ if not own:
+ return
+ writer.heading('Records', '=')
+ writer.paragraph(
+ 'What an op places by name. A named record is placed with record or '
+ 'spliced with $include; a field of records: is placed with a bare '
+ 'record and has one type wherever it is.')
+ for record in own:
+ writer.heading(record.name, '-')
+ if record.doc:
+ writer.paragraph(record.doc)
+ if record.role == 'field':
+ field = record.fields[0]
+ writer.block(f'record {record.record_id:#010x}, field '
+ f'{field.number}: {_kind_text(family, field)}')
+ continue
+ rows = [f'record {record.record_id:#010x}', '']
+ rows += _record_rows(family, descendants, record, 0, (record.name,))
+ writer.block(*rows)
+
+
+def _record_rows(family: semantics.Family,
+ descendants: tuple[semantics.Family, ...],
+ record: semantics.Record, depth: int,
+ opened: tuple[str, ...] = ()) -> list[str]:
+ """One line per member of a record, in spec order, records expanded.
+
+ opened are the named records above, which a record of one re-enters.
+ """
+ rows = []
+ included = None
+ for member in record.members:
+ field = member.field
+ origin = None
+ if field.record_id != record.record_id and not field.shared:
+ origin = field.record
+ if origin is not None and origin != included:
+ source = family.records[origin]
+ rows.append(f'{" " * depth}$include {origin}, record '
+ f'{source.record_id:#010x}')
+ included = origin
+ inner, reenters = None, False
+ if field.decl.kind == 'record' and field.decl.extension is None:
+ assert field.holds is not None
+ inner = family.records[field.holds]
+ reenters = inner.role == 'named' and inner.name in opened
+ name = field.name + _suffix(member.required, field.decl)
+ rows += _row(family, name, field, depth, _doc(family, member),
+ reenters)
+ if inner is not None and not reenters:
+ below = opened + ((inner.name,) if inner.role == 'named'
+ else ())
+ rows += _record_rows(family, descendants, inner, depth + 1,
+ below)
+ if field.decl.extension is not None:
+ rows += _fill_rows(family, descendants,
+ family.extensions[field.decl.extension],
+ depth + 1)
+ return rows
+
+
+def _doc(family: semantics.Family, member: semantics.Member) -> str | None:
+ """A member's doc: where it is placed, else where it is declared."""
+ if member.doc:
+ return member.doc
+ if member.field.decl.doc:
+ return member.field.decl.doc
+ if member.field.shared:
+ return family.records[member.field.record].doc
+ return None
+
+
+def _extensions(writer: _Writer, family: semantics.Family,
+ descendants: tuple[semantics.Family, ...]) -> None:
+ own = family.local_extensions
+ if not own:
+ return
+ writer.heading('Extension points', '=')
+ writer.paragraph(
+ 'A place a descendant adds members to, by declaring a record of the '
+ 'same name. It is empty on this family\'s own devices; a descendant '
+ 'that fills it lengthens every message that reaches it.')
+ rows = []
+ for extension in own:
+ fillers = []
+ for child in descendants:
+ fill = child.fill_of(extension)
+ if fill is not None:
+ fillers.append(child.owners[fill.owner_family_id])
+ filled = ', filled by ' + ', '.join(fillers) if fillers else ''
+ rows.append(f'{extension.name}, ID {extension.ext_id}{filled}')
+ writer.block(*rows)
+
+
+def _fills(writer: _Writer, family: semantics.Family,
+ descendants: tuple[semantics.Family, ...]) -> None:
+ if not family.fills:
+ return
+ writer.heading('Extension points this family fills', '=')
+ writer.paragraph(
+ 'Each of these completes an extension point an ancestor declared. '
+ 'What it adds appears in every message of every op that reaches that '
+ 'point, so an inherited op admits a longer message on a device of '
+ 'this family than on a device of the family that declared it.')
+ for fill in family.fills:
+ owner = family.owners.get(fill.family_id, str(fill.family_id))
+ writer.heading(f'{fill.name} of {owner}', '-')
+ if fill.record.doc:
+ writer.paragraph(fill.record.doc)
+ rows = [f'record {fill.record.record_id:#010x}', '']
+ rows += _record_rows(family, descendants, fill.record, 0)
+ writer.block(*rows)
+
+
+def _ops(writer: _Writer, family: semantics.Family,
+ descendants: tuple[semantics.Family, ...]) -> None:
+ own = family.local_ops
+ if not own:
+ return
+ writer.heading('Ops', '=')
+ writer.paragraph(
+ 'One section per op this family declares. An action is executed and '
+ 'acknowledged, and a query is executed and answered in as many '
+ 'fragments as its answer needs. In short:')
+ lines = []
+ for op in own:
+ lines += _signature(family, op, LINE_LIMIT - BLOCK_INDENT)
+ writer.block(*lines)
+ for op in own:
+ _op(writer, family, descendants, op)
+
+
+def _op(writer: _Writer, family: semantics.Family,
+ descendants: tuple[semantics.Family, ...], op: semantics.Op) -> None:
+ writer.heading(op.name, '-')
+ writer.field('Op ID', f'``{_id(op.op_id)}``, op {op.local_id}')
+ writer.field('Type', TYPES[op.type])
+ writer.field('FD access', _fd_access(op.fd_access))
+ writer.field('Availability', _availability(op.availability))
+ writer.line()
+ if op.doc:
+ writer.paragraph(op.doc)
+ _message(writer, family, descendants, 'Request', op.request_record)
+ _message(writer, family, descendants, 'Reply', op.reply_record)
+ _sizes(writer, op)
+
+
+def _availability(availability: str) -> str:
+ if availability == 'dynamic':
+ return 'dynamic, so a device may disable and enable it at any time'
+ return 'fixed, so a device that publishes it always answers it'
+
+
+def _fd_access(fd_access: str) -> str:
+ if fd_access == 'read':
+ return 'read, so an observation or full-control fd may execute it'
+ return 'read-write, so only a full-control fd may execute it'
+
+
+def _message(writer: _Writer, family: semantics.Family,
+ descendants: tuple[semantics.Family, ...], what: str,
+ record: semantics.Record | None) -> None:
+ if record is None:
+ writer.paragraph(f'{what}: none.')
+ return
+ if record.role == 'named':
+ writer.paragraph(f'{what}: record {record.name}, '
+ f'{record.record_id:#010x}, as Records has it.')
+ return
+ if record.doc:
+ writer.paragraph(record.doc)
+ rows = _record_rows(family, descendants, record, 0)
+ if not rows:
+ writer.paragraph(f'{what}: an empty root, record '
+ f'{record.record_id:#010x}.')
+ return
+ writer.paragraph(f'{what}, record {record.record_id:#010x}:')
+ writer.block(*rows)
+
+
+def _fill_rows(family: semantics.Family,
+ descendants: tuple[semantics.Family, ...],
+ extension: semantics.Extension, depth: int) -> list[str]:
+ """What each family below puts into one extension point."""
+ rows = []
+ for child in (family, *descendants):
+ fill = child.fill_of(extension)
+ if fill is None or (child is not family and
+ fill.owner_family_id != child.family_id):
+ continue
+ owner = child.owners[fill.owner_family_id]
+ rows.append(f'{" " * depth}{owner}: record '
+ f'{fill.record.record_id:#010x} of family '
+ f'{fill.owner_family_id}')
+ rows += _record_rows(child, descendants, fill.record, depth + 1)
+ return rows
+
+
+def _row(family: semantics.Family, name: str, field: semantics.Field,
+ depth: int, doc: str | None, reenters: bool = False) -> list[str]:
+ """One field: name, number and schema kind, its doc below."""
+ indent = ' ' * depth
+ label = f'{indent}{name}'
+ number = str(field.number)
+ what = _kind_text(family, field, reenters)
+ if field.shared:
+ what += f', record {field.record_id:#010x}'
+ column = NAME_WIDTH + NUMBER_WIDTH
+ parts = _wrap(what, LINE_LIMIT - BLOCK_INDENT - column) or ['']
+ if len(label) >= NAME_WIDTH:
+ rows = [label, f'{" " * NAME_WIDTH}{number:<{NUMBER_WIDTH}}{parts[0]}']
+ else:
+ rows = [f'{label:<{NAME_WIDTH}}{number:<{NUMBER_WIDTH}}{parts[0]}']
+ rows += [f'{" " * column}{part}' for part in parts[1:]]
+ if doc:
+ doc_indent = indent + ' ' * DOC_INDENT
+ width = LINE_LIMIT - BLOCK_INDENT - len(doc_indent)
+ rows += [f'{doc_indent}{line}' for line in _wrap(doc, width)]
+ return rows
+
+
+def _suffix(required: bool, decl: spec.FieldDecl) -> str:
+ """The [N] and ? a spec writes after a field name."""
+ suffix = ''
+ if decl.count_enum is not None:
+ suffix = f'[{decl.count_enum}]'
+ elif decl.max_occurrences == spec.UNLIMITED:
+ suffix = '[]'
+ elif decl.max_occurrences != 1:
+ suffix = f'[{decl.max_occurrences}]'
+ return suffix + ('' if required else '?')
+
+
+def _kind_text(family: semantics.Family, field: semantics.Field,
+ reenters: bool = False) -> str:
+ """A field's schema kind and bounds, as a spec reads."""
+ decl = field.decl
+ if decl.kind in ('uint', 'sint'):
+ return decl.kind + _bounds(_integer_bounds(decl), _unit(decl))
+ if decl.kind == 'bool':
+ return 'bool'
+ if decl.kind in ('string', 'binary'):
+ return decl.kind + _bounds(_length_bounds(decl), ())
+ if decl.kind == 'enum':
+ assert decl.ref is not None
+ return decl.ref
+ if decl.kind == 'flags':
+ assert decl.ref is not None
+ names = ', '.join(name for name, _ in
+ family.definitions[decl.ref].values)
+ return f'{decl.ref}: {names}'
+ if decl.kind == 'blob':
+ assert decl.blob is not None
+ return 'blob' + _bounds((), _blob_parts(decl.blob))
+ if decl.extension is not None:
+ return f'extension {decl.extension}'
+ assert field.holds is not None
+ record = family.records[field.holds]
+ what = f'record {record.record_id:#010x}'
+ if record.role == 'named':
+ what = f'record {record.name} {record.record_id:#010x}'
+ if reenters:
+ what += ', which this re-enters'
+ return what
+
+
+def _bounds(range_parts: tuple[str, ...], other_parts: tuple[str, ...]) -> str:
+ parts = list(range_parts) + list(other_parts)
+ return f' {", ".join(parts)}' if parts else ''
+
+
+def _integer_bounds(decl: spec.FieldDecl) -> tuple[str, ...]:
+ low = None if decl.min == _default_min(decl.kind) else decl.min
+ high = None if decl.max == _default_max(decl.kind) else decl.max
+ if low is None and high is None:
+ return ()
+ text = spec.size_text if decl.unit == 'bytes' else spec.number_text
+ low_text = text(low) if low is not None else ''
+ high_text = text(high) if high is not None else ''
+ return (f'{low_text}..{high_text}',)
+
+
+def _default_min(kind: str) -> int:
+ return 0 if kind == 'uint' else spec.S64_MIN
+
+
+def _default_max(kind: str) -> int:
+ return spec.U64_MAX if kind == 'uint' else spec.S64_MAX
+
+
+def _unit(decl: spec.FieldDecl) -> tuple[str, ...]:
+ if decl.unit is None:
+ return ()
+ parts = [decl.unit]
+ if decl.scale:
+ parts.append(f'scale {decl.scale}')
+ return tuple(parts)
+
+
+def _length_bounds(decl: spec.FieldDecl) -> tuple[str]:
+ assert decl.min_length is not None and decl.max_length is not None
+ if decl.min_length == decl.max_length:
+ return (spec.size_text(decl.max_length),)
+ low = spec.size_text(decl.min_length) if decl.min_length else ''
+ return (f'{low}..{spec.size_text(decl.max_length)}',)
+
+
+def _blob_parts(blob: spec.BlobDecl) -> tuple[str, ...]:
+ parts = [DIRECTIONS[blob.direction]]
+ if blob.max_length != spec.U32_MAX:
+ parts.append(f'..{spec.size_text(blob.max_length)}')
+ if blob.alignment != 1:
+ parts.append(f'align {spec.size_text(blob.alignment)}')
+ parts.append(' '.join(blob.backings))
+ return tuple(parts)
+
+
+def _sizes(writer: _Writer, op: semantics.Op) -> None:
+ """What a caller has to bring and what it gets back, in bytes."""
+ reported = (
+ ('a schema-known request of at most {} bytes',
+ op.sizes.request_max_len),
+ ('a reply of at most {} bytes', op.sizes.action_reply_max_len),
+ ('a reply buffer of at least {} bytes',
+ op.sizes.query_reply_min_len),
+ ('{} bytes of reply buffer suggested',
+ op.sizes.query_reply_suggested_len),
+ )
+ rows = [label.format(value) for label, value in reported if value]
+ if not rows:
+ return
+ writer.paragraph('Lengths on a device of this family:')
+ writer.block(*rows)
+
+
+def _messages(writer: _Writer, family: semantics.Family,
+ descendants: tuple[semantics.Family, ...]) -> None:
+ if not family.messages:
+ return
+ writer.heading('Messages no op declares', '=')
+ writer.paragraph(
+ 'The core writes these itself, so they belong to no op: a '
+ 'caller reads one out of the buffer it offered for a reply.')
+ for message in family.messages:
+ writer.heading(message.name, '-')
+ record = family.records[message.name]
+ writer.block(f'record {message.record_id:#010x}', '',
+ *_record_rows(family, descendants, record, 0,
+ (record.name,)))
+ writer.paragraph(f'At most {message.max_len} bytes, bounded by '
+ f'{message.limit}.')
+
+
+def _signature(family: semantics.Family, op: semantics.Op,
+ width: int) -> list[str]:
+ """One op as a call: what it takes, and what it answers."""
+ request = _members(family, op.request_record) if op.request_record \
+ else []
+ call = (f'{op.name}(', request, ')')
+ if op.reply_record is None:
+ return _layout((call,), width)
+ return _layout((call, ' -> ', _shape(family, op.reply_record)), width)
+
+
+_Piece = Union[str, tuple[str, list['_Piece'], str]]
+
+
+def _shape(family: semantics.Family, record: semantics.Record) -> _Piece:
+ """A message root: a named record by name, else its members."""
+ if record.role == 'named':
+ return record.name
+ members = _members(family, record)
+ return ('{ ', members, ' }') if members else '{}'
+
+
+def _layout(pieces: tuple[_Piece, ...], width: int) -> list[str]:
+ """One signature, wrapped inside its braces where it is too long.
+
+ A piece is text or a group, an opener, its members and a closer. A member
+ that does not fit goes on a line of its own, below the first one; a reply
+ that does not fit goes on an indented line.
+ """
+ lines = ['']
+ for piece in pieces:
+ if piece == ' -> ':
+ lines[-1] += piece
+ continue
+ if lines[-1].endswith(' -> ') and \
+ len(lines[-1]) + len(_flat(piece)) > width:
+ lines[-1] = lines[-1].rstrip()
+ lines.append(' ')
+ _place(piece, lines, width)
+ return lines
+
+
+def _place(item: _Piece, lines: list[str], width: int) -> None:
+ """Append item to lines, wrapping inside it where it does not fit."""
+ flat = _flat(item)
+ if isinstance(item, str) or len(lines[-1]) + len(flat) <= width:
+ lines[-1] += flat
+ return
+ opener, members, closer = item
+ lines[-1] += opener
+ inner = len(lines[-1])
+ for index, member in enumerate(members):
+ tail = closer if index == len(members) - 1 else ','
+ if index:
+ if len(lines[-1]) + 1 + len(_flat(member)) + len(tail) <= width:
+ lines[-1] += ' '
+ else:
+ lines.append(' ' * inner)
+ _place(member, lines, width - len(tail))
+ lines[-1] += tail
+
+
+def _flat(item: _Piece) -> str:
+ if isinstance(item, str):
+ return item
+ opener, members, closer = item
+ return opener + ', '.join(_flat(member) for member in members) + closer
+
+
+def _members(family: semantics.Family,
+ record: semantics.Record) -> list[_Piece]:
+ """Each member of one record, a record written out in place."""
+ parts: list[_Piece] = []
+ included = None
+ for member in record.members:
+ field = member.field
+ origin = None
+ if field.record_id != record.record_id and not field.shared:
+ origin = field.record
+ if origin is not None:
+ if origin != included:
+ parts.append(origin)
+ included = origin
+ continue
+ included = None
+ name = field.name + _suffix(member.required, field.decl)
+ parts.append(_member(family, field, name))
+ return parts
+
+
+def _member(family: semantics.Family, field: semantics.Field,
+ name: str) -> _Piece:
+ decl = field.decl
+ if decl.kind == 'blob':
+ assert decl.blob is not None
+ return f'{name}: {DIRECTIONS[decl.blob.direction]} blob'
+ if decl.kind != 'record':
+ return name
+ if decl.extension is not None:
+ return f'{name}: {decl.extension}'
+ assert field.holds is not None
+ record = family.records[field.holds]
+ if record.role == 'named':
+ return f'{name}: {record.name}'
+ return (f'{name}: {{ ', _members(family, record), ' }')
+
+
+def _id(value: int) -> str:
+ return f'0x{value:016x}'
diff --git a/tools/ctlv/ctlvgen/lib/gen_runtime.py b/tools/ctlv/ctlvgen/lib/gen_runtime.py
new file mode 100644
index 000000000000..bfb65d7d7b54
--- /dev/null
+++ b/tools/ctlv/ctlvgen/lib/gen_runtime.py
@@ -0,0 +1,66 @@
+# SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
+"""Resolved names and values consumed by specification-aware tools."""
+
+from __future__ import annotations
+
+import json
+from typing import Any
+
+from . import semantics
+
+
+def _definition(item: semantics.Definition) -> dict[str, Any]:
+ values = []
+ for name, number in item.values:
+ value = 1 << number if item.kind == 'flags' else number
+ values.append({'name': name, 'value': value})
+ return {
+ 'name': item.name,
+ 'kind': item.kind,
+ 'values': values,
+ }
+
+
+def _field(item: semantics.Field) -> dict[str, str]:
+ out = {
+ 'name': item.full_name,
+ 'short': item.name,
+ 'type': f'0x{item.type:016x}',
+ 'kind': item.decl.kind,
+ }
+ if item.decl.kind in ('enum', 'flags'):
+ assert item.decl.ref is not None
+ out['definition'] = item.decl.ref
+ return out
+
+
+def _op(item: semantics.Op) -> dict[str, Any]:
+ return {
+ 'name': item.name,
+ 'id': f'0x{item.op_id:016x}',
+ 'type': item.type,
+ 'availability': item.availability,
+ 'request-max-len': item.sizes.request_max_len,
+ 'action-reply-max-len': item.sizes.action_reply_max_len,
+ 'query-reply-min-len': item.sizes.query_reply_min_len,
+ 'query-reply-suggested-len':
+ item.sizes.query_reply_suggested_len,
+ 'error-info-max-len': item.sizes.error_info_max_len,
+ 'fd-access': item.fd_access,
+ }
+
+
+def render(family: semantics.Family) -> str:
+ """Return a versioned manifest for one effective family."""
+ chain = [{'name': name, 'id': family_id}
+ for family_id, name in family.owners.items()]
+ data = {
+ 'version': 1,
+ 'family': family.name,
+ 'chain': chain,
+ 'definitions': [_definition(item)
+ for item in family.definitions.values()],
+ 'fields': [_field(item) for item in family.fields.values()],
+ 'ops': [_op(item) for item in family.ops],
+ }
+ return json.dumps(data, separators=(',', ':')) + '\n'
--
2.56.0
prev parent reply other threads:[~2026-11-13 15:56 UTC|newest]
Thread overview: 16+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-11-13 15:54 [PATCH RFC v2 00/15] ctlv: Add a schema-driven TLV uAPI for character devices Jiri Pirko
2026-11-13 15:55 ` [PATCH RFC v2 01/15] ctlv: Add the schema language and generator Jiri Pirko
2026-11-13 15:55 ` [PATCH RFC v2 02/15] ctlv: Add the UAPI message format and family API Jiri Pirko
2026-11-13 15:55 ` [PATCH RFC v2 03/15] ctlv: Add the root family specification and generated files Jiri Pirko
2026-11-13 15:55 ` [PATCH RFC v2 04/15] Documentation: ctlv: Add userspace ABI Jiri Pirko
2026-11-13 15:55 ` [PATCH RFC v2 05/15] ctlv: Prepare message input handling Jiri Pirko
2026-11-13 15:55 ` [PATCH RFC v2 06/15] ctlv: Prepare blob descriptor validation Jiri Pirko
2026-11-13 15:55 ` [PATCH RFC v2 07/15] ctlv: Prepare schema validation Jiri Pirko
2026-11-13 15:55 ` [PATCH RFC v2 08/15] ctlv: Prepare message output handling Jiri Pirko
2026-11-13 15:55 ` [PATCH RFC v2 09/15] ctlv: Prepare VA blob handling Jiri Pirko
2026-11-13 15:55 ` [PATCH RFC v2 10/15] ctlv: Prepare runtime introspection Jiri Pirko
2026-11-13 15:55 ` [PATCH RFC v2 11/15] ctlv: Add operation dispatch Jiri Pirko
2026-11-13 15:55 ` [PATCH RFC v2 12/15] ctlv: Add character device nodes Jiri Pirko
2026-11-13 15:55 ` [PATCH RFC v2 13/15] Documentation: ctlv: Describe how to write a family Jiri Pirko
2026-11-13 15:55 ` [PATCH RFC v2 14/15] tools/ctlv: Add the userspace library Jiri Pirko
2026-11-13 15:55 ` Jiri Pirko [this message]
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261113155515.2510194-16-jiri@resnulli.us \
--to=jiri@resnulli.us \
--cc=James.Bottomley@HansenPartnership.com \
--cc=aik@amd.com \
--cc=alistair.francis@wdc.com \
--cc=aneesh.kumar@kernel.org \
--cc=ankita@nvidia.com \
--cc=cedric.xing@intel.com \
--cc=dave.hansen@linux.intel.com \
--cc=dave.jiang@intel.com \
--cc=david.quigley@intel.com \
--cc=drdeeglaze@gmail.com \
--cc=elena.reshetova@intel.com \
--cc=jarkko@kernel.org \
--cc=jgg@ziepe.ca \
--cc=jic23@kernel.org \
--cc=jpiotrowski@nvidia.com \
--cc=kas@kernel.org \
--cc=ksinyuk@kernel.org \
--cc=kvm@vger.kernel.org \
--cc=linux-api@vger.kernel.org \
--cc=linux-coco@lists.linux.dev \
--cc=linux-doc@vger.kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=lukas@wunner.de \
--cc=nik.borisov@suse.com \
--cc=peter.fang@intel.com \
--cc=pgonda@google.com \
--cc=qinkun@google.com \
--cc=rick.p.edgecombe@intel.com \
--cc=rodrigo.vivi@intel.com \
--cc=sameo@meta.com \
--cc=sathyanarayanan.kuppuswamy@linux.intel.com \
--cc=sergii.dmytruk@3mdeb.com \
--cc=suzuki.poulose@arm.com \
--cc=x86@kernel.org \
--cc=yeoreum.yun@arm.com \
--cc=zohar@linux.ibm.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®