expo: Support building an expo from a description file

The only way to create an expo at present is by calling the functions to
create each object. It is useful to have more data-driven approach, where
the objects can be specified in a suitable file format and created from
that. This makes testing easier as well.

Add support for describing an expo in a devicetree node. This allows more
complex tests to be set up, as well as providing an easier format for
users. It also provides a better basis for the upcoming configuration
editor.

Signed-off-by: Simon Glass <sjg@chromium.org>
diff --git a/boot/Makefile b/boot/Makefile
index f94c31d..28c4e55 100644
--- a/boot/Makefile
+++ b/boot/Makefile
@@ -50,7 +50,7 @@
 obj-$(CONFIG_SPL_LOAD_FIT) += common_fit.o
 endif
 
-obj-$(CONFIG_$(SPL_TPL_)EXPO) += expo.o scene.o scene_menu.o
+obj-$(CONFIG_$(SPL_TPL_)EXPO) += expo.o scene.o scene_menu.o expo_build.o
 
 obj-$(CONFIG_$(SPL_TPL_)BOOTMETH_VBE) += vbe.o
 obj-$(CONFIG_$(SPL_TPL_)BOOTMETH_VBE_REQUEST) += vbe_request.o
diff --git a/boot/expo.c b/boot/expo.c
index 8c6fbc0..db837f7 100644
--- a/boot/expo.c
+++ b/boot/expo.c
@@ -58,6 +58,7 @@
 
 uint resolve_id(struct expo *exp, uint id)
 {
+	log_debug("resolve id %d\n", id);
 	if (!id)
 		id = exp->next_id++;
 	else if (id >= exp->next_id)
diff --git a/boot/expo_build.c b/boot/expo_build.c
new file mode 100644
index 0000000..7e61ab0
--- /dev/null
+++ b/boot/expo_build.c
@@ -0,0 +1,400 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Building an expo from an FDT description
+ *
+ * Copyright 2022 Google LLC
+ * Written by Simon Glass <sjg@chromium.org>
+ */
+
+#define LOG_CATEGORY	LOGC_EXPO
+
+#include <common.h>
+#include <expo.h>
+#include <fdtdec.h>
+#include <log.h>
+#include <malloc.h>
+#include <dm/ofnode.h>
+#include <linux/libfdt.h>
+
+/**
+ * struct build_info - Information to use when building
+ *
+ * @str_for_id: String for each ID in use, NULL if empty. The string is NULL
+ *	if there is nothing for this ID. Since ID 0 is never used, the first
+ *	element of this array is always NULL
+ * @str_count: Number of entries in @str_for_id
+ */
+struct build_info {
+	const char **str_for_id;
+	int str_count;
+};
+
+/**
+ * add_txt_str - Add a string or lookup its ID, then add to expo
+ *
+ * @info: Build information
+ * @node: Node describing scene
+ * @scn: Scene to add to
+ * @find_name: Name to look for (e.g. "title"). This will find a property called
+ * "title" if it exists, else will look up the string for "title-id"
+ * Return: ID of added string, or -ve on error
+ */
+int add_txt_str(struct build_info *info, ofnode node, struct scene *scn,
+		const char *find_name, uint obj_id)
+{
+	const char *text;
+	uint str_id;
+	int ret;
+
+	text = ofnode_read_string(node, find_name);
+	if (!text) {
+		char name[40];
+		u32 id;
+
+		snprintf(name, sizeof(name), "%s-id", find_name);
+		ret = ofnode_read_u32(node, name, &id);
+		if (ret)
+			return log_msg_ret("id", -EINVAL);
+
+		if (id >= info->str_count)
+			return log_msg_ret("id", -E2BIG);
+		text = info->str_for_id[id];
+		if (!text)
+			return log_msg_ret("id", -EINVAL);
+	}
+
+	ret = expo_str(scn->expo, find_name, 0, text);
+	if (ret < 0)
+		return log_msg_ret("add", ret);
+	str_id = ret;
+
+	ret = scene_txt_str(scn, find_name, obj_id, str_id, text, NULL);
+	if (ret < 0)
+		return log_msg_ret("add", ret);
+
+	return ret;
+}
+
+/**
+ * add_txt_str_list - Add a list string or lookup its ID, then add to expo
+ *
+ * @info: Build information
+ * @node: Node describing scene
+ * @scn: Scene to add to
+ * @find_name: Name to look for (e.g. "title"). This will find a string-list
+ * property called "title" if it exists, else will look up the string in the
+ * "title-id" string list.
+ * Return: ID of added string, or -ve on error
+ */
+int add_txt_str_list(struct build_info *info, ofnode node, struct scene *scn,
+		     const char *find_name, int index, uint obj_id)
+{
+	const char *text;
+	uint str_id;
+	int ret;
+
+	ret = ofnode_read_string_index(node, find_name, index, &text);
+	if (ret) {
+		char name[40];
+		u32 id;
+
+		snprintf(name, sizeof(name), "%s-id", find_name);
+		ret = ofnode_read_u32_index(node, name, index, &id);
+		if (ret)
+			return log_msg_ret("id", -ENOENT);
+
+		if (id >= info->str_count)
+			return log_msg_ret("id", -E2BIG);
+		text = info->str_for_id[id];
+		if (!text)
+			return log_msg_ret("id", -EINVAL);
+	}
+
+	ret = expo_str(scn->expo, find_name, 0, text);
+	if (ret < 0)
+		return log_msg_ret("add", ret);
+	str_id = ret;
+
+	ret = scene_txt_str(scn, find_name, obj_id, str_id, text, NULL);
+	if (ret < 0)
+		return log_msg_ret("add", ret);
+
+	return ret;
+}
+
+/*
+ * build_element() - Handle creating a text object from a label
+ *
+ * Look up a property called @label or @label-id and create a string for it
+ */
+int build_element(void *ldtb, int node, const char *label)
+{
+	return 0;
+}
+
+/**
+ * read_strings() - Read in the list of strings
+ *
+ * Read the strings into an ID-indexed list, so they can be used for building
+ * an expo. The strings are in a /strings node and each has its own subnode
+ * containing the ID and the string itself:
+ *
+ * example {
+ *    id = <123>;
+ *    value = "This is a test";
+ * };
+ *
+ * Future work may add support for unicode and multiple languages
+ *
+ * @info: Build information
+ * @root: Root node to read from
+ * Returns: 0 if OK, -ENOMEM if out of memory, -EINVAL if there is a format
+ * error
+ */
+static int read_strings(struct build_info *info, ofnode root)
+{
+	ofnode strings, node;
+
+	strings = ofnode_find_subnode(root, "strings");
+	if (!ofnode_valid(strings))
+		return log_msg_ret("str", -EINVAL);
+
+	ofnode_for_each_subnode(node, strings) {
+		const char *val;
+		int ret;
+		u32 id;
+
+		ret = ofnode_read_u32(node, "id", &id);
+		if (ret)
+			return log_msg_ret("id", -EINVAL);
+		val = ofnode_read_string(node, "value");
+		if (!val)
+			return log_msg_ret("val", -EINVAL);
+
+		if (id >= info->str_count) {
+			int new_count = info->str_count + 20;
+			void *new_arr;
+
+			new_arr = realloc(info->str_for_id,
+					  new_count * sizeof(char *));
+			if (!new_arr)
+				return log_msg_ret("id", -ENOMEM);
+			memset(new_arr + info->str_count, '\0',
+			       (new_count - info->str_count) * sizeof(char *));
+			info->str_for_id = new_arr;
+			info->str_count = new_count;
+		}
+
+		info->str_for_id[id] = val;
+	}
+
+	return 0;
+}
+
+/**
+ * list_strings() - List the available strings with their IDs
+ *
+ * @info: Build information
+ */
+static void list_strings(struct build_info *info)
+{
+	int i;
+
+	for (i = 0; i < info->str_count; i++) {
+		if (info->str_for_id[i])
+			printf("%3d %s\n", i, info->str_for_id[i]);
+	}
+}
+
+/**
+ * menu_build() - Build a menu and add it to a scene
+ *
+ * See doc/developer/expo.rst for a description of the format
+ *
+ * @info: Build information
+ * @node: Node containing the menu description
+ * @scn: Scene to add the menu to
+ * Returns: 0 if OK, -ENOMEM if out of memory, -EINVAL if there is a format
+ * error, -ENOENT if there is a references to a non-existent string
+ */
+static int menu_build(struct build_info *info, ofnode node, struct scene *scn)
+{
+	struct scene_obj_menu *menu;
+	uint title_id, menu_id;
+	const u32 *item_ids;
+	int ret, size, i;
+	const char *name;
+	u32 id;
+
+	name = ofnode_get_name(node);
+	ret = ofnode_read_u32(node, "id", &id);
+	if (ret)
+		return log_msg_ret("id", -EINVAL);
+
+	ret = scene_menu(scn, name, id, &menu);
+	if (ret < 0)
+		return log_msg_ret("men", ret);
+	menu_id = ret;
+
+	/* Set the title */
+	ret = add_txt_str(info, node, scn, "title", 0);
+	if (ret < 0)
+		return log_msg_ret("tit", ret);
+	title_id = ret;
+	ret = scene_menu_set_title(scn, menu_id, title_id);
+
+	item_ids = ofnode_read_prop(node, "item-id", &size);
+	if (!item_ids)
+		return log_msg_ret("itm", -EINVAL);
+	if (!size || size % sizeof(u32))
+		return log_msg_ret("isz", -EINVAL);
+	size /= sizeof(u32);
+
+	for (i = 0; i < size; i++) {
+		struct scene_menitem *item;
+		uint label, key, desc;
+
+		ret = add_txt_str_list(info, node, scn, "item-label", i, 0);
+		if (ret < 0 && ret != -ENOENT)
+			return log_msg_ret("lab", ret);
+		label = max(0, ret);
+
+		ret = add_txt_str_list(info, node, scn, "key-label", i, 0);
+		if (ret < 0 && ret != -ENOENT)
+			return log_msg_ret("key", ret);
+		key = max(0, ret);
+
+		ret = add_txt_str_list(info, node, scn, "desc-label", i, 0);
+		if (ret < 0  && ret != -ENOENT)
+			return log_msg_ret("lab", ret);
+		desc = max(0, ret);
+
+		ret = scene_menuitem(scn, menu_id, simple_xtoa(i),
+				     fdt32_to_cpu(item_ids[i]), key, label,
+				     desc, 0, 0, &item);
+		if (ret < 0)
+			return log_msg_ret("mi", ret);
+	}
+
+	return 0;
+}
+
+/**
+ * menu_build() - Build an expo object and add it to a scene
+ *
+ * See doc/developer/expo.rst for a description of the format
+ *
+ * @info: Build information
+ * @node: Node containing the object description
+ * @scn: Scene to add the object to
+ * Returns: 0 if OK, -ENOMEM if out of memory, -EINVAL if there is a format
+ * error, -ENOENT if there is a references to a non-existent string
+ */
+static int obj_build(struct build_info *info, ofnode node, struct scene *scn)
+{
+	const char *type;
+	u32 id;
+	int ret;
+
+	log_debug("- object %s\n", ofnode_get_name(node));
+	ret = ofnode_read_u32(node, "id", &id);
+	if (ret)
+		return log_msg_ret("id", -EINVAL);
+
+	type = ofnode_read_string(node, "type");
+	if (!type)
+		return log_msg_ret("typ", -EINVAL);
+
+	if (!strcmp("menu", type))
+		ret = menu_build(info, node, scn);
+	 else
+		ret = -EINVAL;
+	if (ret)
+		return log_msg_ret("bld", ret);
+
+	return 0;
+}
+
+/**
+ * scene_build() - Build a scene and all its objects
+ *
+ * See doc/developer/expo.rst for a description of the format
+ *
+ * @info: Build information
+ * @node: Node containing the scene description
+ * @scn: Scene to add the object to
+ * Returns: 0 if OK, -ENOMEM if out of memory, -EINVAL if there is a format
+ * error, -ENOENT if there is a references to a non-existent string
+ */
+static int scene_build(struct build_info *info, ofnode scn_node,
+		       struct expo *exp)
+{
+	const char *name;
+	struct scene *scn;
+	uint id, title_id;
+	ofnode node;
+	int ret;
+
+	name = ofnode_get_name(scn_node);
+	log_debug("Building scene %s\n", name);
+	ret = ofnode_read_u32(scn_node, "id", &id);
+	if (ret)
+		return log_msg_ret("id", -EINVAL);
+
+	ret = scene_new(exp, name, id, &scn);
+	if (ret < 0)
+		return log_msg_ret("scn", ret);
+
+	ret = add_txt_str(info, scn_node, scn, "title", 0);
+	if (ret < 0)
+		return log_msg_ret("tit", ret);
+	title_id = ret;
+	scene_title_set(scn, title_id);
+
+	ret = add_txt_str(info, scn_node, scn, "prompt", 0);
+	if (ret < 0)
+		return log_msg_ret("pr", ret);
+
+	ofnode_for_each_subnode(node, scn_node) {
+		ret = obj_build(info, node, scn);
+		if (ret < 0)
+			return log_msg_ret("mit", ret);
+	}
+
+	return 0;
+}
+
+int expo_build(ofnode root, struct expo **expp)
+{
+	struct build_info info;
+	ofnode scenes, node;
+	struct expo *exp;
+	u32 dyn_start;
+	int ret;
+
+	memset(&info, '\0', sizeof(info));
+	ret = read_strings(&info, root);
+	if (ret)
+		return log_msg_ret("str", ret);
+	list_strings(&info);
+
+	ret = expo_new("name", NULL, &exp);
+	if (ret)
+		return log_msg_ret("exp", ret);
+
+	if (!ofnode_read_u32(root, "dynamic-start", &dyn_start))
+		expo_set_dynamic_start(exp, dyn_start);
+
+	scenes = ofnode_find_subnode(root, "scenes");
+	if (!ofnode_valid(scenes))
+		return log_msg_ret("sno", -EINVAL);
+
+	ofnode_for_each_subnode(node, scenes) {
+		ret = scene_build(&info, node, exp);
+		if (ret < 0)
+			return log_msg_ret("scn", ret);
+	}
+	*expp = exp;
+
+	return 0;
+}