futility: updater: create a manifest from the signer config

The signer_config.csv contains almost everything we need for mapping
from models to image files so that can be an alternative when
models/*/setvars.sh files are not available. This can be shared by
/firmware in future.

Verified by comparing the manifest.json from existing firmware updaters
on the following boards: volteer (share target), sarien (no ec), octopus
(custom-label), and hatch (share target).

 for BOARD in volteer sarien octopus hatch; do
   /build/$BOARD/usr/sbin/chromeos-firmwareupdate --unpack test/$BOARD
   jq -S . <test/$BOARD/manifest.json >old
   rm -rf test/$BOARD/models
   futility update --manifest -a test/$BOARD | jq -S . >new
   diff new old >diff.$BOARD
 done

 # all diff.$BOARD are empty, e.g., no difference.

BUG=b:230679721
TEST=make; run test
BRANCH=None

Change-Id: I17393c941d2577a2b1c5f00f60b25e98fa3f3a6f
Signed-off-by: Hung-Te Lin <hungte@chromium.org>
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform/vboot_reference/+/3612646
Reviewed-by: Julius Werner <jwerner@chromium.org>
Commit-Queue: Yu-Ping Wu <yupingso@chromium.org>
diff --git a/futility/updater_archive.c b/futility/updater_archive.c
index 58774fc..4ed12aa 100644
--- a/futility/updater_archive.c
+++ b/futility/updater_archive.c
@@ -85,6 +85,7 @@
 		  * const VPD_CUSTOMIZATION_ID = "customization_id",
 		  * const ENV_VAR_MODEL_DIR = "${MODEL_DIR}",
 		  * const PATH_STARTSWITH_KEYSET = "keyset/",
+		  * const PATH_SIGNER_CONFIG = "signer_config.csv",
 		  * const PATH_ENDSWITH_SETVARS = "/setvars.sh";
 
 struct archive {
@@ -571,7 +572,7 @@
 	uint8_t *data;
 	uint32_t len;
 
-	char *ptr_line, *ptr_token;
+	char *ptr_line = NULL, *ptr_token = NULL;
 	char *line, *k, *v;
 	int valid = 0;
 
@@ -867,6 +868,118 @@
 	return !manifest_add_model(manifest, &model);
 }
 
+/* Returns the matched model config from the manifest, or NULL if not found. */
+static struct model_config *manifest_get_model_config(
+		const struct manifest *manifest, const char *name)
+{
+	int i = 0;
+
+	for (i = 0; i < manifest->num; i++) {
+		if (!strcmp(name, manifest->models[i].name))
+			return &manifest->models[i];
+	}
+	return NULL;
+}
+
+/*
+ * Creates the manifest from the 'signer_config.csv' file.
+ * Returns 0 on success (loaded), otherwise failure.
+ */
+static int manifest_from_signer_config(struct manifest *manifest)
+{
+	struct archive *archive = manifest->archive;
+	uint32_t size;
+	uint8_t *data;
+	char *s, *tok_ptr = NULL;
+
+	if (!archive_has_entry(archive, PATH_SIGNER_CONFIG))
+		return -1;
+
+	/*
+	 * CSV format: model_name,firmware_image,key_id,ec_image
+	 *
+	 * Note the key_id is not signature_id and won't be used, and ec_image
+	 * may be optional (for example sarien).
+	 */
+
+	if (archive_read_file(archive, PATH_SIGNER_CONFIG, &data, &size,NULL)) {
+		ERROR("Failed reading: %s\n", PATH_SIGNER_CONFIG);
+		return -1;
+	}
+
+	/* Skip headers. */
+	s = strtok_r((char *)data, "\n", &tok_ptr);
+	if (!s || !strchr(s, ',')) {
+		ERROR("Invalid %s: missing header.\n", PATH_SIGNER_CONFIG);
+		free(data);
+		return -1;
+	}
+
+	for (s = strtok_r(NULL, "\n", &tok_ptr); s != NULL;
+	     s = strtok_r(NULL, "\n", &tok_ptr)) {
+
+		struct model_config model = {0};
+		int discard_model = 0;
+
+		/*
+		 * Both keyid (%3) and ec_image (%4) are optional so we want to
+		 * read at least 2 fields.
+		 */
+		if (sscanf(s, "%m[^,],%m[^,],%*[^,],%m[^,]",
+		    &model.name, &model.image, &model.ec_image) < 2) {
+			ERROR("Invalid entry(%s): %s\n", PATH_SIGNER_CONFIG, s);
+			discard_model = 1;
+		} else if (strchr(model.name, '-')) {
+			/* format: BaseModel-CustomLabel */
+			char *tok_dash;
+			char *base_model;
+			struct model_config *base_model_config;
+
+			VB2_DEBUG("Found custom-label: %s\n", model.name);
+			discard_model = 1;
+			base_model = strtok_r(model.name, "-", &tok_dash);
+			assert(base_model);
+
+			/*
+			 * Currently we assume the base model (e.g., base_model)
+			 * is always listed before CL models in the CSV file -
+			 * this is based on how the signerbot and the
+			 * chromeos-config works today (validated on octopus).
+			 */
+			base_model_config = manifest_get_model_config(
+					manifest, base_model);
+
+			if (!base_model_config) {
+				ERROR("Invalid CL-model: %s\n", base_model);
+			} else if (!base_model_config->is_custom_label) {
+				base_model_config->is_custom_label = 1;
+				/*
+				 * Rewriting signature_id is not necessary,
+				 * but in order to generate the same manifest
+				 * from setvars, we want to temporarily use
+				 * the special value.
+				 */
+				free(base_model_config->signature_id);
+				base_model_config->signature_id = strdup(
+						"sig-id-in-customization-id");
+			}
+		}
+
+		if (discard_model) {
+			free(model.name);
+			free(model.image);
+			free(model.ec_image);
+			continue;
+		}
+
+		model.signature_id = strdup(model.name);
+		if (!manifest_add_model(manifest, &model))
+			break;
+	}
+	free(data);
+	return 0;
+}
+
 /**
  * get_manifest_key() - Wrapper to get the firmware manifest key from crosid
  *
@@ -927,10 +1040,8 @@
 		model_name = manifest_key;
 	}
 
-	for (i = 0; !model && i < manifest->num; i++) {
-		if (strcmp(model_name, manifest->models[i].name) == 0)
-			model = &manifest->models[i];
-	}
+	model = manifest_get_model_config(manifest, model_name);
+
 	if (!model) {
 		ERROR("Unsupported model: '%s'.\n", model_name);
 
@@ -1054,14 +1165,25 @@
 
 	manifest.archive = archive;
 	manifest.default_model = -1;
+
+	VB2_DEBUG("Try to build a manifest from *%s\n", PATH_ENDSWITH_SETVARS);
 	archive_walk(archive, &manifest, manifest_scan_entries);
-	if (manifest.num == 0)
+
+	if (manifest.num == 0) {
+		VB2_DEBUG("Try to build a manifest from %s\n",
+			  PATH_SIGNER_CONFIG);
+		manifest_from_signer_config(&manifest);
+	}
+	if (manifest.num == 0) {
+		VB2_DEBUG("Try to build a manifest from a */firmware folder\n");
 		archive_walk(archive, &manifest, manifest_scan_raw_entries);
+	}
 
 	if (manifest.num == 0) {
 		const char *image_name = NULL;
 		struct firmware_image image = {0};
 
+		VB2_DEBUG("Try to build a manifest from a simple folder\n");
 		/* Try to load from current folder. */
 		if (archive_has_entry(archive, old_host_image_name))
 			image_name = old_host_image_name;