diff --git a/README.md b/README.md index a9b4f71..0af1b1d 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,28 @@ These can of course be combined with e.g. *--serial*. A subset of the installer package can be selected for installation by appending a **::storage1[,storage2...]** suffix to the file name. +### Flashing contents.xml + +QDL also supports flashing builds described by *contents.xml* files: + +```bash +qdl flash contents.xml +``` + +As the contents XML can describe the content for multiple storage types and +multiple flavors, it might be necessary to select which content to flash. This +is done by appending the **::specifier1,specifier2...** suffix to the file +name. The specifier is matched against **storage types** and **flavors**. At +most one resolved specifier per storage is allowed, and only the selected parts +are flashed. As an example: + +```bash +qdl flash contents.xml::ufs,safe_rtos +``` + +will flash the UFS storage with the only applicable flavor, and will flash +*safe_rtos* onto the spinor. + ### Flash simulation (dry run) Use the `--dry-run` option to run QDL without connecting to or flashing any diff --git a/contents.c b/contents.c new file mode 100644 index 0000000..e2bd7b8 --- /dev/null +++ b/contents.c @@ -0,0 +1,1003 @@ +// SPDX-License-Identifier: BSD-3-Clause +/* + * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. + */ +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "contents.h" +#include "file.h" +#include "firehose.h" +#include "pathbuf.h" +#include "qdl.h" + +enum contents_file_type { + CONTENTS_FILE_OTHER, + CONTENTS_FILE_PROGRAM, + CONTENTS_FILE_PATCH, + CONTENTS_FILE_DEVICE_PROGRAMMER, + CONTENTS_FILE_PROGRAMMER_XML, +}; + +enum firehose_type { + FIREHOSE_TYPE_NONE, + FIREHOSE_TYPE_LITE, + FIREHOSE_TYPE_TRUE, +}; + +struct contents_entry { + enum contents_file_type file_type; + + enum qdl_storage_type storage_type; + char *flavor; + + char *filename; + struct pathbuf path; + + enum firehose_type firehose_type; + + struct list_head node; +}; + +struct contents { + struct list_head entries; + struct pathbuf base_dir; + + char **flavors; + size_t num_flavors; +}; + +struct contents_filter { + struct contents *contents; + + enum qdl_storage_type storage_type; + const char *flavor; +}; + +struct contents_selector { + enum qdl_storage_type storage_type; + const char *flavor; +}; + +static const char *contents_storage_name(enum qdl_storage_type storage) +{ + const char *name = encode_storage_type(storage); + + return name ? name : "unknown"; +} + +static char *contents_node_get_text(xmlNode *node) +{ + const xmlChar *start; + const xmlChar *end; + xmlChar *str; + size_t len; + char *ret; + + str = xmlNodeGetContent(node); + if (!str) + return NULL; + + for (start = str; *start && isspace((unsigned char)*start); start++) + ; + + for (end = start + xmlStrlen(start); end > start && + isspace((unsigned char)end[-1]); end--) + ; + + len = end - start; + ret = calloc(1, len + 1); + if (!ret) + goto out_free_str; + + memcpy(ret, start, len); + +out_free_str: + xmlFree(str); + + return ret; +} + +static bool contents_entry_is_ignored(xmlNode *node) +{ + xmlChar *ignore; + bool ret; + + ignore = xmlGetProp(node, (xmlChar *)"ignore"); + if (!ignore) + return false; + + ret = !xmlStrcmp(ignore, (xmlChar *)"true"); + + xmlFree(ignore); + + return ret; +} + +static int contents_parse_pf(struct contents *contents, xmlNode *node) +{ + char **new_flavors; + char *name = NULL; + + for (; node; node = node->next) { + if (node->type == XML_ELEMENT_NODE && + !xmlStrcmp(node->name, (xmlChar *)"name")) { + name = contents_node_get_text(node); + break; + } + } + + if (!name) + return -1; + + new_flavors = realloc(contents->flavors, (contents->num_flavors + 1) * sizeof(*new_flavors)); + if (!new_flavors) { + free(name); + return -1; + } + + contents->flavors = new_flavors; + contents->flavors[contents->num_flavors++] = name; + + return 0; +} + +static int contents_parse_pfs(struct contents *contents, xmlNode *node) +{ + int ret; + + for (; node; node = node->next) { + if (node->type != XML_ELEMENT_NODE) + continue; + + if (!xmlStrcmp(node->name, (xmlChar *)"pf")) { + ret = contents_parse_pf(contents, node->children); + if (ret < 0) { + ux_err("failed to parse product flavor definition\n"); + return -1; + } + } + } + + return 0; +} + +static enum contents_file_type contents_detect_file_type(xmlNode *node, + enum firehose_type *fh_type) +{ + enum contents_file_type type = CONTENTS_FILE_OTHER; + xmlChar *firehose_type; + + if (!xmlStrcmp(node->name, (xmlChar *)"partition_file")) + type = CONTENTS_FILE_PROGRAM; + if (!xmlStrcmp(node->name, (xmlChar *)"partition_patch_file")) + type = CONTENTS_FILE_PATCH; + if (!xmlStrcmp(node->name, (xmlChar *)"device_programmer")) + type = CONTENTS_FILE_DEVICE_PROGRAMMER; + + firehose_type = xmlGetProp(node, (xmlChar *)"firehose_type"); + if (firehose_type) { + if (!xmlStrcmp(firehose_type, (xmlChar *)"true")) { + type = CONTENTS_FILE_PROGRAMMER_XML; + *fh_type = FIREHOSE_TYPE_TRUE; + } else if (!xmlStrcmp(firehose_type, (xmlChar *)"lite")) { + *fh_type = FIREHOSE_TYPE_LITE; + } else { + *fh_type = FIREHOSE_TYPE_NONE; + } + } + + xmlFree(firehose_type); + + return type; +} + +static enum qdl_storage_type contents_detect_storage_type(xmlNode *node) +{ + enum qdl_storage_type type = QDL_STORAGE_UNKNOWN; + xmlChar *storage; + + storage = xmlGetProp(node, (xmlChar *)"storage_type"); + if (storage) { + type = decode_storage_type((char *)storage); + xmlFree(storage); + } + + return type; +} + +static int contents_expand_path_vars(struct pathbuf *path) +{ + char *head = &path->buf[0]; + char *tail = head; + char *colon; + char *end; + + /* + * Paths can contain "${var:default}" entries, which allow overriding + * portions of the path. As this is not supported, replace each with + * "default". + */ + while (*head) { + if (head[0] == '$' && head[1] == '{') { + colon = strchr(head + 2, ':'); + end = colon ? strchr(colon + 1, '}') : NULL; + if (colon && end) { + head = colon + 1; + while (head < end) + *tail++ = *head++; + head = end + 1; + continue; + } + } + *tail++ = *head++; + } + + *tail = '\0'; + path->len = tail - path->buf; + + return 0; +} + +static int contents_parse_file_names(struct contents *contents, xmlNode *node, + enum contents_file_type file_type, + struct pathbuf *current_path, char *path_flavor, + enum qdl_storage_type storage, + enum firehose_type firehose_type) +{ + struct contents_entry *entry; + struct pathbuf full_path; + xmlNode *child; + char *filename; + int ret; + + for (child = node->children; child; child = child->next) { + if (child->type != XML_ELEMENT_NODE) + continue; + + if (xmlStrcmp(child->name, (xmlChar *)"file_name")) + continue; + + filename = contents_node_get_text(child); + if (!filename) + return -1; + + /* Ignore filenames with wildcards */ + if (strstr(filename, "*")) { + free(filename); + continue; + } + + qdl_pathbuf_dup(&full_path, current_path); + ret = qdl_pathbuf_push(&full_path, filename); + if (ret < 0) { + free(filename); + return -1; + } + + contents_expand_path_vars(&full_path); + + entry = calloc(1, sizeof(*entry)); + if (!entry) { + free(filename); + return -1; + } + + entry->file_type = file_type; + entry->storage_type = storage; + if (path_flavor) { + entry->flavor = strdup(path_flavor); + if (!entry->flavor) { + free(entry); + free(filename); + return -1; + } + } + entry->firehose_type = firehose_type; + + entry->filename = filename; + qdl_pathbuf_dup(&entry->path, &full_path); + + list_append(&contents->entries, &entry->node); + } + + return 0; +} + +static int contents_parse_entry(struct contents *contents, xmlNode *node, + struct pathbuf *build_root) +{ + enum contents_file_type file_type; + enum firehose_type firehose_type = FIREHOSE_TYPE_NONE; + enum qdl_storage_type storage; + struct pathbuf file_path; + xmlNode *child; + char *flavor; + char *path; + int ret; + + if (!build_root) { + ux_err("entry has no build root path in contents.xml\n"); + return -1; + } + + if (contents_entry_is_ignored(node)) + return 0; + + file_type = contents_detect_file_type(node, &firehose_type); + storage = contents_detect_storage_type(node); + + for (child = node->children; child; child = child->next) { + if (child->type != XML_ELEMENT_NODE) + continue; + + if (xmlStrcmp(child->name, (xmlChar *)"file_path")) + continue; + + path = contents_node_get_text(child); + if (!path) + return -1; + flavor = (char *)xmlGetProp(child, (xmlChar *)"flavor"); + + qdl_pathbuf_dup(&file_path, build_root); + ret = qdl_pathbuf_push(&file_path, path); + if (ret < 0) { + xmlFree(flavor); + free(path); + return -1; + } + + ret = contents_parse_file_names(contents, node, file_type, &file_path, flavor, storage, + firehose_type); + + xmlFree(flavor); + free(path); + if (ret < 0) + return ret; + } + + return 0; +} + +static bool contents_is_entry_node(xmlNode *node) +{ + return !xmlStrcmp(node->name, (xmlChar *)"download_file") || + !xmlStrcmp(node->name, (xmlChar *)"file_ref") || + !xmlStrcmp(node->name, (xmlChar *)"partition_file") || + !xmlStrcmp(node->name, (xmlChar *)"partition_patch_file") || + !xmlStrcmp(node->name, (xmlChar *)"device_programmer"); +} + +static int contents_parse_builds(struct contents *contents, xmlNode *node, struct pathbuf *root_path) +{ + char *root; + int ret; + + for (; node; node = node->next) { + if (node->type != XML_ELEMENT_NODE) + continue; + + if (!xmlStrcmp(node->name, (xmlChar *)"build")) { + struct pathbuf build_root = {0}; + + ret = contents_parse_builds(contents, node->children, &build_root); + if (ret < 0) + return ret; + + continue; + } + + if (!xmlStrcmp(node->name, (xmlChar *)"linux_root_path")) { + root = contents_node_get_text(node); + if (!root) + return -1; + + if (!root_path) { + ux_err("linux_root_path without active build context\n"); + free(root); + return -1; + } + + qdl_pathbuf_dup(root_path, &contents->base_dir); + ret = qdl_pathbuf_push(root_path, root); + free(root); + if (ret < 0) + return -1; + } else if (contents_is_entry_node(node)) { + ret = contents_parse_entry(contents, node, root_path); + if (ret < 0) + return ret; + } + } + + return 0; +} + +static int contents_parse_nodes(struct contents *contents, xmlNode *node) +{ + int ret; + + for (; node; node = node->next) { + if (node->type != XML_ELEMENT_NODE) + continue; + + if (!xmlStrcmp(node->name, (xmlChar *)"product_flavors")) { + ret = contents_parse_pfs(contents, node->children); + if (ret < 0) + return ret; + } else if (!xmlStrcmp(node->name, (xmlChar *)"builds_flat")) { + ret = contents_parse_builds(contents, node->children, NULL); + if (ret < 0) + return ret; + } + } + + return 0; +} + +static int contents_get_base_dir(struct pathbuf *base_dir, const char *filename) +{ + qdl_pathbuf_reset(base_dir); + qdl_pathbuf_push(base_dir, filename); + qdl_pathbuf_dirname(base_dir); + + return 0; +} + +int contents_load_xml(struct contents *contents, const char *filename) +{ + xmlNode *root; + xmlDoc *doc; + int ret; + + doc = xmlReadFile(filename, NULL, 0); + if (!doc) { + ux_err("failed to parse contents file \"%s\"\n", filename); + return -1; + } + + root = xmlDocGetRootElement(doc); + if (!root || xmlStrcmp(root->name, (xmlChar *)"contents")) { + ux_err("specified file \"%s\" is not a contents.xml document\n", filename); + xmlFreeDoc(doc); + return -1; + } + + ret = contents_get_base_dir(&contents->base_dir, filename); + if (ret < 0) + goto err_free_doc; + + ret = contents_parse_nodes(contents, root->children); + if (ret < 0) + goto err_free_doc; + + xmlFreeDoc(doc); + + if (list_empty(&contents->entries)) + ux_info("contents: no file entries parsed from \"%s\"\n", filename); + + return 0; + +err_free_doc: + xmlFreeDoc(doc); + return -1; +} + +static int contents_find_programmers(struct contents *contents, struct sahara_image *images) +{ + struct contents_filter filter = { .contents = contents }; + struct contents_entry *entry; + struct sahara_image blob; + int ret; + + list_for_each_entry(entry, &contents->entries, node) { + if (entry->file_type != CONTENTS_FILE_PROGRAMMER_XML) + continue; + + ret = load_sahara_image(NULL, qdl_pathbuf_str(&entry->path), &blob); + if (ret < 0) { + ux_err("unable to open \"%s\" for reading\n", qdl_pathbuf_str(&entry->path)); + continue; + } + + ret = decode_sahara_config(&blob, images, &filter); + if (ret == 0) { + ux_err("%s is not a programmer xml\n", qdl_pathbuf_str(&entry->path)); + sahara_images_free(&blob, 1); + continue; + } else if (ret < 0) { + ux_err("failed to parse programmer xml \"%s\"\n", qdl_pathbuf_str(&entry->path)); + return -1; + } + + return 0; + } + + list_for_each_entry(entry, &contents->entries, node) { + if (entry->file_type != CONTENTS_FILE_DEVICE_PROGRAMMER) + continue; + + if (entry->firehose_type == FIREHOSE_TYPE_LITE) + continue; + + ret = load_sahara_image(NULL, qdl_pathbuf_str(&entry->path), + &images[SAHARA_ID_EHOSTDL_IMG]); + if (ret < 0) { + ux_err("unable to open \"%s\" for reading\n", qdl_pathbuf_str(&entry->path)); + continue; + } + + return 0; + } + + ux_err("no programmer definitions found\n"); + return -1; +} + +static bool contents_flavor_matches(const char *a, const char *b) +{ + return (!a && !b) || (a && b && !strcmp(a, b)); +} + +static bool contents_selector_is_known(struct contents_selector *selectors, size_t count, + enum qdl_storage_type storage_type, + const char *flavor) +{ + size_t i; + + for (i = 0; i < count; i++) { + if (selectors[i].storage_type == storage_type && + contents_flavor_matches(selectors[i].flavor, flavor)) + return true; + } + + return false; +} + +static bool contents_storage_is_selected(struct contents_selector *selectors, size_t count, + enum qdl_storage_type storage_type) +{ + size_t selector_idx; + + for (selector_idx = 0; selector_idx < count; selector_idx++) { + if (selectors[selector_idx].storage_type == storage_type) + return true; + } + + return false; +} + +static bool contents_storage_has_flavored_entries(struct contents *contents, + enum qdl_storage_type storage_type) +{ + struct contents_entry *entry; + + list_for_each_entry(entry, &contents->entries, node) { + if (entry->file_type != CONTENTS_FILE_PROGRAM && + entry->file_type != CONTENTS_FILE_PATCH) + continue; + if (entry->storage_type != storage_type) + continue; + if (entry->flavor) + return true; + } + + return false; +} + +static size_t contents_collect_valid_selectors(struct contents *contents, + struct contents_selector **contents_selectors) +{ + struct contents_selector *new_selectors; + struct contents_selector *selectors = NULL; + struct contents_entry *entry; + size_t count = 0; + + list_for_each_entry(entry, &contents->entries, node) { + if (entry->file_type != CONTENTS_FILE_PROGRAM && + entry->file_type != CONTENTS_FILE_PATCH) + continue; + if (entry->storage_type == QDL_STORAGE_UNKNOWN) + continue; + if (!entry->flavor && contents_storage_has_flavored_entries(contents, entry->storage_type)) + continue; + if (contents_selector_is_known(selectors, count, entry->storage_type, entry->flavor)) + continue; + + new_selectors = realloc(selectors, (count + 1) * sizeof(*selectors)); + if (!new_selectors) { + free(selectors); + return 0; + } + + selectors = new_selectors; + selectors[count].storage_type = entry->storage_type; + selectors[count].flavor = entry->flavor; + count++; + } + + *contents_selectors = selectors; + + return count; +} + +static void contents_print_valid_selectors(struct contents_selector *selectors, size_t count) +{ + size_t selector_idx; + + ux_err("valid storage/flavor combinations:\n"); + for (selector_idx = 0; selector_idx < count; selector_idx++) { + if (selectors[selector_idx].flavor) + ux_err(" %s/%s\n", contents_storage_name(selectors[selector_idx].storage_type), + selectors[selector_idx].flavor); + else + ux_err(" %s\n", contents_storage_name(selectors[selector_idx].storage_type)); + } +} + +static bool contents_flavor_is_valid(struct contents *contents, const char *flavor) +{ + size_t flavor_idx; + + for (flavor_idx = 0; flavor_idx < contents->num_flavors; flavor_idx++) { + if (!strcmp(flavor, contents->flavors[flavor_idx])) + return true; + } + + return false; +} + +static size_t contents_select_by_storage(struct contents_selector *valid_selectors, + size_t num_valid_selectors, + enum qdl_storage_type storage_type, + struct contents_selector *selector) +{ + size_t valid_idx; + size_t matches = 0; + + for (valid_idx = 0; valid_idx < num_valid_selectors; valid_idx++) { + if (valid_selectors[valid_idx].storage_type != storage_type) + continue; + + *selector = valid_selectors[valid_idx]; + matches++; + } + + return matches; +} + +static size_t contents_select_by_flavor(struct contents_selector *valid_selectors, + size_t num_valid_selectors, + const char *flavor, + struct contents_selector *selector) +{ + size_t valid_idx; + size_t matches = 0; + + for (valid_idx = 0; valid_idx < num_valid_selectors; valid_idx++) { + if (!valid_selectors[valid_idx].flavor || + strcmp(valid_selectors[valid_idx].flavor, flavor)) + continue; + + *selector = valid_selectors[valid_idx]; + matches++; + } + + return matches; +} + +static int contents_decode_selectors(struct contents *contents, char *pattern, + struct contents_selector **contents_selectors) +{ + struct contents_selector *valid_selectors = NULL; + struct contents_selector *new_selectors; + struct contents_selector *selectors = NULL; + struct contents_selector selector; + enum qdl_storage_type storage; + size_t num_valid_selectors; + char *flavor; + size_t count = 0; + char *token; + char *save; + char *sep; + size_t matches; + + num_valid_selectors = contents_collect_valid_selectors(contents, &valid_selectors); + if (!num_valid_selectors) { + ux_err("contents.xml does not provide any valid storage/flavor combinations\n"); + return -1; + } + + if (!pattern) { + if (num_valid_selectors == 1) { + *contents_selectors = valid_selectors; + return 1; + } + + if (num_valid_selectors > 1) { + ux_err("contents.xml contains multiple storage/flavor combinations; select one or more with ::/\n"); + contents_print_valid_selectors(valid_selectors, num_valid_selectors); + free(valid_selectors); + return -1; + } + + free(valid_selectors); + return -1; + } + + if (!pattern[0]) { + ux_err("empty storage/flavor selector\n"); + goto err_free_valid_selectors; + } + + for (token = strtok_r(pattern, ",", &save); token; token = strtok_r(NULL, ",", &save)) { + new_selectors = realloc(selectors, (count + 1) * sizeof(*selectors)); + if (!new_selectors) + goto err_free_selectors; + + selectors = new_selectors; + + if (!token[0]) { + ux_err("empty storage/flavor selector\n"); + goto err_free_selectors; + } + + sep = strchr(token, '/'); + if (!sep) { + storage = decode_storage_type(token); + if (storage != QDL_STORAGE_UNKNOWN) { + matches = contents_select_by_storage(valid_selectors, num_valid_selectors, + storage, &selector); + if (matches == 1) + goto append_selector; + + if (!matches) + ux_err("storage type \"%s\" has no valid flavor in contents.xml\n", token); + else + ux_err("storage type \"%s\" is ambiguous; specify a flavor\n", token); + + contents_print_valid_selectors(valid_selectors, num_valid_selectors); + goto err_free_selectors; + } + + matches = contents_select_by_flavor(valid_selectors, num_valid_selectors, + token, &selector); + if (matches == 1) + goto append_selector; + + if (matches > 1) + ux_err("flavor \"%s\" is ambiguous; specify a storage type\n", token); + else if (contents_flavor_is_valid(contents, token)) + ux_err("flavor \"%s\" has no valid storage type in contents.xml\n", token); + else + ux_err("unknown storage type or flavor \"%s\"\n", token); + + contents_print_valid_selectors(valid_selectors, num_valid_selectors); + goto err_free_selectors; + } + + *sep = '\0'; + + flavor = sep + 1; + if (!token[0]) { + ux_err("missing storage selector for flavor \"%s\"\n", flavor); + goto err_free_selectors; + } + if (!flavor[0]) { + ux_err("invalid flavor selection for storage \"%s\"\n", token); + goto err_free_selectors; + } + + if (strchr(flavor, '/')) { + ux_err("invalid flavor selector \"%s\"\n", flavor); + goto err_free_selectors; + } + + if (!contents_flavor_is_valid(contents, flavor)) { + ux_err("invalid flavor \"%s\" requested\n", flavor); + ux_err("valid flavors:\n"); + + for (size_t flavor_idx = 0; flavor_idx < contents->num_flavors; flavor_idx++) + ux_err(" %s\n", contents->flavors[flavor_idx]); + goto err_free_selectors; + } + + storage = decode_storage_type(token); + if (storage == QDL_STORAGE_UNKNOWN) { + ux_err("unknown storage type \"%s\"\n", token); + goto err_free_selectors; + } + + if (!contents_selector_is_known(valid_selectors, num_valid_selectors, storage, flavor)) { + ux_err("storage/flavor combination \"%s/%s\" not mentioned in contents.xml\n", + contents_storage_name(storage), flavor); + contents_print_valid_selectors(valid_selectors, num_valid_selectors); + goto err_free_selectors; + } + + selector.storage_type = storage; + selector.flavor = flavor; + +append_selector: + if (contents_storage_is_selected(selectors, count, selector.storage_type)) { + ux_err("storage type \"%s\" selected multiple times\n", + contents_storage_name(selector.storage_type)); + goto err_free_selectors; + } + + selectors[count] = selector; + count++; + } + + *contents_selectors = selectors; + free(valid_selectors); + + return count; + +err_free_selectors: + free(selectors); +err_free_valid_selectors: + free(valid_selectors); + + return -1; +} + +int contents_load(struct list_head *ops, const char *filename, char *specifier, + struct sahara_image *images, const char *incdir) +{ + struct contents_filter filter = {}; + struct contents_entry *entry; + struct contents_entry *next; + struct contents contents = {}; + enum qdl_storage_type storage_type; + struct contents_selector *selectors = NULL; + struct firehose_op *op; + const char *flavor; + int num_selectors; + char *pattern = specifier; + size_t flavor_idx; + int ret; + int i; + + list_init(&contents.entries); + ret = contents_load_xml(&contents, filename); + if (ret < 0) + goto out_free_contents; + + ret = contents_decode_selectors(&contents, pattern, &selectors); + if (ret < 0) + goto out_free_contents; + num_selectors = ret; + if (num_selectors == 0) { + ux_err("contents.xml does not provide any valid storage/flavor combinations\n"); + ret = -1; + goto out_free_contents; + } + + ret = contents_find_programmers(&contents, images); + if (ret < 0) + goto out_free_contents; + + for (i = 0; i < num_selectors; i++) { + storage_type = selectors[i].storage_type; + flavor = selectors[i].flavor; + + op = firehose_alloc_op(FIREHOSE_OP_CONFIGURE); + op->storage_type = storage_type; + list_append(ops, &op->node); + + list_for_each_entry(entry, &contents.entries, node) { + if (entry->file_type != CONTENTS_FILE_PROGRAM) + continue; + if (entry->storage_type != QDL_STORAGE_UNKNOWN && entry->storage_type != storage_type) + continue; + if (entry->flavor && (!flavor || strcmp(entry->flavor, flavor))) + continue; + + filter.contents = &contents; + filter.storage_type = storage_type; + filter.flavor = flavor; + + ret = program_load(ops, qdl_pathbuf_str(&entry->path), + storage_type == QDL_STORAGE_NAND, false, &filter, incdir); + if (ret < 0) { + ux_err("failed to load program: %s\n", entry->filename); + goto out_free_contents; + } + } + + list_for_each_entry(entry, &contents.entries, node) { + if (entry->file_type != CONTENTS_FILE_PATCH) + continue; + if (entry->storage_type != QDL_STORAGE_UNKNOWN && entry->storage_type != storage_type) + continue; + if (entry->flavor && (!flavor || strcmp(entry->flavor, flavor))) + continue; + + ret = patch_load(ops, qdl_pathbuf_str(&entry->path)); + if (ret < 0) { + ux_err("failed to load %s\n", entry->filename); + goto out_free_contents; + } + } + } + +out_free_contents: + for (flavor_idx = 0; flavor_idx < contents.num_flavors; flavor_idx++) + free(contents.flavors[flavor_idx]); + free(contents.flavors); + + list_for_each_entry_safe(entry, next, &contents.entries, node) { + free(entry->filename); + free(entry->flavor); + free(entry); + } + free(selectors); + + return ret; +} + +int contents_resolve_path(struct contents_filter *filter, const char *filename, struct pathbuf *path) +{ + enum qdl_storage_type storage_type; + struct contents_entry *entry; + struct contents *contents; + struct pathbuf probe; + const char *flavor; + + if (!filter) + return 0; + + contents = filter->contents; + storage_type = filter->storage_type; + flavor = filter->flavor; + + /* Look for a match */ + list_for_each_entry(entry, &contents->entries, node) { + if (entry->storage_type != QDL_STORAGE_UNKNOWN && entry->storage_type != storage_type) { + continue; + } + if (entry->flavor && flavor && strcmp(entry->flavor, flavor)) { + continue; + } + + if (strcmp(entry->filename, filename)) + continue; + + qdl_pathbuf_dup(path, &entry->path); + return 1; + } + + /* + * fh_loader adds all applicable to the search path, to find + * files adjacent to those described in the contents.xml. So if we + * didn't find an exact match, search adjacent to all other files... + */ + list_for_each_entry(entry, &contents->entries, node) { + if (entry->storage_type != QDL_STORAGE_UNKNOWN && entry->storage_type != storage_type) { + continue; + } + if (entry->flavor && flavor && strcmp(entry->flavor, flavor)) { + continue; + } + + qdl_pathbuf_dup(&probe, &entry->path); + qdl_pathbuf_dirname(&probe); + qdl_pathbuf_push(&probe, filename); + + if (!access(probe.buf, F_OK)) { + qdl_pathbuf_dup(path, &probe); + return 1; + } + } + + return 0; +} diff --git a/contents.h b/contents.h new file mode 100644 index 0000000..5f75dd6 --- /dev/null +++ b/contents.h @@ -0,0 +1,21 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ +/* + * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. + */ +#ifndef __CONTENTS_H__ +#define __CONTENTS_H__ + +#include +#include + +#include "list.h" + +struct sahara_image; +struct pathbuf; +struct contents_filter; + +int contents_load(struct list_head *ops, const char *filename, char *specifier, + struct sahara_image *images, const char *incdir); +int contents_resolve_path(struct contents_filter *filter, const char *filename, struct pathbuf *path); + +#endif diff --git a/firehose.c b/firehose.c index 9aebbec..87de4cc 100644 --- a/firehose.c +++ b/firehose.c @@ -316,23 +316,21 @@ static int firehose_send_configure(struct qdl_device *qdl, size_t payload_size, enum qdl_storage_type storage, size_t *max_payload_size) { - static const char * const memory_names[] = { - [QDL_STORAGE_EMMC] = "emmc", - [QDL_STORAGE_NAND] = "nand", - [QDL_STORAGE_UFS] = "ufs", - [QDL_STORAGE_NVME] = "nvme", - [QDL_STORAGE_SPINOR] = "spinor", - }; + const char *memory_name; xmlNode *root; xmlNode *node; xmlDoc *doc; + memory_name = encode_storage_type(storage); + if (!memory_name) + return -EINVAL; + doc = xmlNewDoc((xmlChar *)"1.0"); root = xmlNewNode(NULL, (xmlChar *)"data"); xmlDocSetRootElement(doc, root); node = xmlNewChild(root, NULL, (xmlChar *)"configure", NULL); - xml_setpropf(node, "MemoryName", memory_names[storage]); + xml_setpropf(node, "MemoryName", memory_name); xml_setpropf(node, "MaxPayloadSizeToTargetInBytes", "%lu", payload_size); xml_setpropf(node, "Verbose", "%d", 0); xml_setpropf(node, "ZlpAwareHost", "%d", 1); diff --git a/flashmap.c b/flashmap.c index 54965b0..c94996f 100644 --- a/flashmap.c +++ b/flashmap.c @@ -2,10 +2,13 @@ /* * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. */ +#include #include +#include #include #include #include +#include #include "file.h" #include "json.h" @@ -19,8 +22,26 @@ enum { QDL_FILE_PROGRAM, }; -static int flashmap_get_programmers(struct qdl_zip *zip, struct json_value *layout, - struct sahara_image *images, const char *incdir) +static int flashmap_resolve_path(char *path, size_t path_size, const char *filename, const char *incdir) +{ + if (!filename) { + ux_err("flashmap: filename is null\n"); + return -1; + } + + if (incdir) { + snprintf(path, path_size, "%s/%s", incdir, filename); + if (access(path, F_OK)) + snprintf(path, path_size, "%s", filename); + } else { + snprintf(path, path_size, "%s", filename); + } + + return 0; +} + +static int flashmap_get_legacy_programmer(struct qdl_zip *zip, struct json_value *layout, + struct sahara_image *images, const char *incdir) { struct json_value *programmers; const char *filename; @@ -28,6 +49,11 @@ static int flashmap_get_programmers(struct qdl_zip *zip, struct json_value *layo int count; programmers = json_get_child(layout, "programmer"); + if (!programmers) { + ux_err("flashmap: parse error when decoding programmer\n"); + return -1; + } + count = json_count_children(programmers); if (count != 1) { ux_err("flashmap: single programmer expected, found %d\n", count); @@ -40,19 +66,77 @@ static int flashmap_get_programmers(struct qdl_zip *zip, struct json_value *layo return -1; } - if (incdir) { - snprintf(path, PATH_MAX, "%s/%s", incdir, filename); - if (access(path, F_OK)) - snprintf(path, PATH_MAX, "%s", filename); - } else { - snprintf(path, PATH_MAX, "%s", filename); - } + if (flashmap_resolve_path(path, sizeof(path), filename, incdir)) + return -1; ux_debug("flashmap: selected programmer: %s\n", path); return load_sahara_image(zip, path, &images[SAHARA_ID_EHOSTDL_IMG]); } +static int flashmap_get_programmer_map(struct qdl_zip *zip, struct json_value *layout, + struct sahara_image *images, const char *incdir) +{ + struct json_value *programmers; + struct json_value *entry; + unsigned long image_id; + const char *filename; + char *end; + char path[PATH_MAX]; + int count = 0; + int ret; + + programmers = json_get_child(layout, "programmer"); + if (!programmers || programmers->type != JSON_TYPE_OBJECT) { + ux_err("flashmap: programmer map must be an object for version 1.2.0-qdl\n"); + return -1; + } + + for (entry = programmers->u.value; entry; entry = entry->next) { + errno = 0; + image_id = strtoul(entry->key, &end, 0); + if (errno || end == entry->key || *end || image_id == 0 || image_id >= MAPPING_SZ) { + ux_err("flashmap: invalid programmer image id \"%s\"\n", entry->key); + return -1; + } + + if (entry->type != JSON_TYPE_STRING || !entry->u.string) { + ux_err("flashmap: programmer entry \"%s\" must be a filename string\n", entry->key); + return -1; + } + + filename = entry->u.string; + ret = flashmap_resolve_path(path, sizeof(path), filename, incdir); + if (ret) + return ret; + + ux_debug("flashmap: selected programmer %lu: %s\n", image_id, path); + + ret = load_sahara_image(zip, path, &images[image_id]); + if (ret) + return ret; + + count++; + } + + if (!count) { + ux_err("flashmap: programmer map is empty\n"); + return -1; + } + + return 0; +} + +static int flashmap_get_programmers(struct qdl_zip *zip, struct json_value *layout, + struct sahara_image *images, const char *incdir, + bool uses_programmer_map) +{ + if (uses_programmer_map) + return flashmap_get_programmer_map(zip, layout, images, incdir); + + return flashmap_get_legacy_programmer(zip, layout, images, incdir); +} + static int flashmap_load_xml(struct list_head *ops, struct qdl_zip *zip, const char *filename, bool is_nand, const char *incdir) { @@ -101,10 +185,12 @@ static int flashmap_load_xml(struct list_head *ops, struct qdl_zip *zip, const c if (!xmlStrcmp(root->name, (xmlChar *)"patches")) { type = QDL_FILE_PATCH; } else if (!xmlStrcmp(root->name, (xmlChar *)"data")) { + type = QDL_FILE_PROGRAM; for (node = root->children; node ; node = node->next) { if (node->type != XML_ELEMENT_NODE) continue; - if (!xmlStrcmp(node->name, (xmlChar *)"program")) { + if (!xmlStrcmp(node->name, (xmlChar *)"program") || + !xmlStrcmp(node->name, (xmlChar *)"erase")) { type = QDL_FILE_PROGRAM; break; } @@ -113,7 +199,7 @@ static int flashmap_load_xml(struct list_head *ops, struct qdl_zip *zip, const c switch (type) { case QDL_FILE_PROGRAM: - ret = program_load_xml(ops, doc, zip, filename, is_nand, false, incdir); + ret = program_load_xml(ops, doc, zip, filename, is_nand, false, NULL, incdir); break; case QDL_FILE_PATCH: ret = patch_load_xml(ops, doc, filename); @@ -176,7 +262,7 @@ static int flashmap_enumerate_programmables(struct json_value *list, struct list is_nand = !strcmp(memory, "nand"); op = firehose_alloc_op(FIREHOSE_OP_CONFIGURE); - op->storage_type = decode_storage(memory); + op->storage_type = decode_storage_type(memory); list_append(ops, &op->node); file_count = json_count_children(files); @@ -192,13 +278,8 @@ static int flashmap_enumerate_programmables(struct json_value *list, struct list return -1; } - if (incdir) { - snprintf(path, PATH_MAX, "%s/%s", incdir, file); - if (access(path, F_OK)) - snprintf(path, PATH_MAX, "%s", file); - } else { - snprintf(path, PATH_MAX, "%s", file); - } + if (flashmap_resolve_path(path, sizeof(path), file, incdir)) + return -1; ret = flashmap_load_xml(ops, zip, path, is_nand, incdir); if (ret) @@ -209,15 +290,27 @@ static int flashmap_enumerate_programmables(struct json_value *list, struct list return 0; } -int flashmap_load(struct list_head *ops, const char *filename, struct sahara_image *images, const char *incdir) +int flashmap_load(struct list_head *ops, const char *filename, char *specifier, + struct sahara_image *images, const char *incdir) { + struct list_head flashmap_ops = LIST_INIT(flashmap_ops); + enum qdl_storage_type current_type = QDL_STORAGE_UNKNOWN; struct json_value *programmable; struct json_value *product; struct json_value *layout; + struct firehose_op *op; + struct firehose_op *next; struct qdl_file flashmap; struct json_value *json; struct json_value *obj; struct qdl_zip *zip; + unsigned int type_filter = 0; + unsigned int matched_ops = 0; + char *filter = specifier; + char *save = NULL; + char *tmp; + enum qdl_storage_type type; + bool uses_programmer_map = false; const char *version; const char *name; size_t json_size; @@ -225,6 +318,21 @@ int flashmap_load(struct list_head *ops, const char *filename, struct sahara_ima int count; int ret = -1; + if (filter) { + for (tmp = strtok_r(filter, ",", &save); tmp; tmp = strtok_r(NULL, ",", &save)) { + type = decode_storage_type(tmp); + if (type == QDL_STORAGE_UNKNOWN) { + ux_err("unknown storage type \"%s\"\n", tmp); + return -1; + } + + type_filter |= 1U << type; + } + } + + if (!type_filter) + type_filter = ~0U; + ret = qdl_zip_open(filename, &zip); if (ret < 0) { ux_err("unable to create zip reference\n"); @@ -249,7 +357,15 @@ int flashmap_load(struct list_head *ops, const char *filename, struct sahara_ima } version = json_get_string(json, "version"); - if (!version || strcmp(version, "1.1.0")) { + if (!version) { + ux_err("unsupported flashmap version\n"); + ret = -1; + goto out_free_json; + } + + if (!strcmp(version, "1.2.0-qdl")) + uses_programmer_map = true; + else if (strcmp(version, "1.1.0")) { ux_err("unsupported flashmap version\n"); ret = -1; goto out_free_json; @@ -282,7 +398,7 @@ int flashmap_load(struct list_head *ops, const char *filename, struct sahara_ima layout = json_get_element_object(obj, 0); - ret = flashmap_get_programmers(zip, layout, images, zip ? NULL : incdir); + ret = flashmap_get_programmers(zip, layout, images, zip ? NULL : incdir, uses_programmer_map); if (ret) goto out_free_json; @@ -294,10 +410,29 @@ int flashmap_load(struct list_head *ops, const char *filename, struct sahara_ima goto out_free_json; } - ret = flashmap_enumerate_programmables(programmable, ops, zip, zip ? NULL : incdir); + ret = flashmap_enumerate_programmables(programmable, &flashmap_ops, zip, zip ? NULL : incdir); if (ret < 0) { - firehose_free_ops(ops); + firehose_free_ops(&flashmap_ops); sahara_images_free(images, MAPPING_SZ); + goto out_free_json; + } + + list_for_each_entry_safe(op, next, &flashmap_ops, node) { + if (op->storage_type != QDL_STORAGE_UNKNOWN) + current_type = op->storage_type; + + if ((1U << current_type) & type_filter) { + list_del(&op->node); + list_append(ops, &op->node); + matched_ops++; + } + } + + firehose_free_ops(&flashmap_ops); + + if (!matched_ops) { + ux_err("loaded flashmap does not contain any operations for selected storage type\n"); + ret = -1; } out_free_json: diff --git a/flashmap.h b/flashmap.h index 6ea2a37..0352366 100644 --- a/flashmap.h +++ b/flashmap.h @@ -10,6 +10,8 @@ struct sahara_image; -int flashmap_load(struct list_head *ops, const char *filename, struct sahara_image *images, const char *incdir); +int flashmap_load(struct list_head *ops, const char *filename, char *specifier, + struct sahara_image *images, const char *incdir); +int zipper_write(const char *filename, struct list_head *ops, struct sahara_image *images); #endif diff --git a/meson.build b/meson.build index fff5ff2..8864ee7 100644 --- a/meson.build +++ b/meson.build @@ -58,7 +58,8 @@ qdl_sources = files( 'firehose.c', 'io.c', 'qdl.c', 'patch.c', 'program.c', 'read.c', 'sha2.c', 'sim.c', 'ufs.c', 'usb.c', - 'vip.c', 'sparse.c', 'gpt.c', 'flashmap.c', 'json.c' + 'vip.c', 'sparse.c', 'gpt.c', 'flashmap.c', 'json.c', 'contents.c', 'pathbuf.c', + 'zipper.c', ) qdl_exe = executable('qdl', @@ -115,6 +116,8 @@ if cmocka_dep.found() test_program_load_xml = executable('test_program_load_xml', sources : [ 'tests/test_program_load_xml.c', + 'tests/common.c', + 'pathbuf.c', 'program.c', 'util.c', version_h, @@ -131,6 +134,58 @@ if cmocka_dep.found() protocol: 'tap', env: ['CMOCKA_MESSAGE_OUTPUT=TAP'], ) + + test_pathbuf = executable('test_pathbuf', + sources : [ + 'tests/test_pathbuf.c', + 'pathbuf.c', + ], + dependencies : common_dep + [cmocka_dep], + include_directories : inc, + ) + + test( + 'pathbuf dirname behavior', + test_pathbuf, + suite: 'unit', + protocol: 'tap', + env: ['CMOCKA_MESSAGE_OUTPUT=TAP'], + ) + + test_contents_selectors = executable('test_contents_selectors', + sources : [ + 'tests/test_contents_selectors.c', + 'pathbuf.c', + ], + dependencies : common_dep + [cmocka_dep], + include_directories : inc, + ) + + test( + 'contents selector resolution', + test_contents_selectors, + suite: 'unit', + protocol: 'tap', + env: ['CMOCKA_MESSAGE_OUTPUT=TAP'], + ) + + test_contents_xml = executable('test_contents_xml', + sources : [ + 'tests/test_contents_xml.c', + 'tests/common.c', + 'pathbuf.c', + ], + dependencies : common_dep + [cmocka_dep], + include_directories : inc, + ) + + test( + 'contents XML parsing', + test_contents_xml, + suite: 'unit', + protocol: 'tap', + env: ['CMOCKA_MESSAGE_OUTPUT=TAP'], + ) else warning('cmocka not found; skipping unit tests') endif diff --git a/pathbuf.c b/pathbuf.c new file mode 100644 index 0000000..b0d55bd --- /dev/null +++ b/pathbuf.c @@ -0,0 +1,182 @@ +// SPDX-License-Identifier: BSD-3-Clause +/* + * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. + */ +#include +#include +#include + +#include "oscompat.h" +#include "pathbuf.h" + +static bool qdl_path_is_sep(char ch) +{ +#ifdef _WIN32 + return ch == '/' || ch == '\\'; +#else + return ch == '/'; +#endif +} + +static char *qdl_pathbuf_last_sep(char *path) +{ + char *last = NULL; + + for (; *path; path++) { + if (qdl_path_is_sep(*path)) + last = path; + } + + return last; +} + +#ifdef _WIN32 +static size_t qdl_pathbuf_unc_root_len(const char *path) +{ + size_t i; + size_t start; + + if (!qdl_path_is_sep(path[0]) || !qdl_path_is_sep(path[1])) + return 0; + + i = 2; + while (qdl_path_is_sep(path[i])) + i++; + if (!path[i]) + return 2; + + start = i; + while (path[i] && !qdl_path_is_sep(path[i])) + i++; + if (i == start) + return 2; + + while (qdl_path_is_sep(path[i])) + i++; + if (!path[i]) + return 2; + + start = i; + while (path[i] && !qdl_path_is_sep(path[i])) + i++; + if (i == start) + return 2; + + return i; +} +#endif + +void qdl_pathbuf_reset(struct pathbuf *path) +{ + if (!path) + return; + + path->buf[0] = '\0'; + path->len = 0; +} + +void qdl_pathbuf_dup(struct pathbuf *dst, const struct pathbuf *orig) +{ + memcpy(dst, orig, sizeof(struct pathbuf)); +} + +int qdl_pathbuf_push(struct pathbuf *path, const char *component) +{ + size_t component_len; + size_t path_len; + size_t skip = 0; + size_t need_sep = 0; + + if (!path || !component) + return -EINVAL; + + if (component[0] == '\0') + return 0; + + path_len = path->len; + if (path_is_absolute(component)) + path_len = 0; + + if (component[0] == '.' && component[1] == '/') + skip += 2; + + if (path_len > 0) + need_sep = (path->buf[path_len - 1] != '/'); + + if (path_len > 0 && !need_sep) { + while (component[skip] == '/') + skip++; + } + + component_len = strlen(component + skip); + if (component_len == 0) + return 0; + + if (path_len + need_sep + component_len + 1 > sizeof(path->buf)) + return -ENAMETOOLONG; + + if (need_sep) + path->buf[path_len++] = '/'; + + memcpy(path->buf + path_len, component + skip, component_len + 1); + path->len = path_len + component_len; + + return 0; +} + +const char *qdl_pathbuf_str(const struct pathbuf *path) +{ + return path ? path->buf : NULL; +} + +void qdl_pathbuf_dirname(struct pathbuf *path) +{ + size_t root_len = 0; + char *sep; + + if (!path || !path->buf[0]) + return; + + if (path->len == 0) + path->len = strlen(path->buf); + +#ifdef _WIN32 + if (isalpha((unsigned char)path->buf[0]) && path->buf[1] == ':' && qdl_path_is_sep(path->buf[2])) { + root_len = 3; + } else { + root_len = qdl_pathbuf_unc_root_len(path->buf); + if (root_len == 0 && qdl_path_is_sep(path->buf[0])) + root_len = 1; + } +#else + if (path->buf[0] == '/') + root_len = 1; +#endif + + while (path->len > root_len && qdl_path_is_sep(path->buf[path->len - 1])) { + path->buf[--path->len] = '\0'; + } + + sep = qdl_pathbuf_last_sep(path->buf); + if (!sep) + return; + + if ((size_t)(sep - path->buf) < root_len) { + path->buf[root_len] = '\0'; + path->len = root_len; + return; + } + + *sep = '\0'; + path->len = (size_t)(sep - path->buf); +} + +bool qdl_path_exists(const struct pathbuf *path) +{ + struct stat st; + + if (!path || !path->buf[0]) + return false; + + return stat(path->buf, &st) == 0; +} diff --git a/pathbuf.h b/pathbuf.h new file mode 100644 index 0000000..b131af4 --- /dev/null +++ b/pathbuf.h @@ -0,0 +1,26 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ +/* + * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. + */ +#ifndef __PATHBUF_H__ +#define __PATHBUF_H__ + +#include +#include +#include + +struct pathbuf { + char buf[PATH_MAX]; + size_t len; +}; + +void qdl_pathbuf_reset(struct pathbuf *path); +void qdl_pathbuf_dup(struct pathbuf *dst, const struct pathbuf *orig); +int qdl_pathbuf_push(struct pathbuf *path, const char *component); +const char *qdl_pathbuf_str(const struct pathbuf *path); + +void qdl_pathbuf_dirname(struct pathbuf *path); + +bool qdl_path_exists(const struct pathbuf *path); + +#endif diff --git a/program.c b/program.c index f34aef8..a0c58c6 100644 --- a/program.c +++ b/program.c @@ -3,6 +3,8 @@ * Copyright (c) 2016-2017, Linaro Ltd. * All rights reserved. */ +#include "contents.h" +#include "pathbuf.h" #include #define _FILE_OFFSET_BITS 64 #include @@ -151,12 +153,13 @@ static int program_load_sparse(struct list_head *ops, struct firehose_op *progra } static int program_resolve_path(struct firehose_op *program, const char *program_file, - const char *incdir) + struct contents_filter *contents_filter, const char *incdir) { char *program_file_copy; char candidate[PATH_MAX]; const char *filename = program->filename; const char *program_dir; + struct pathbuf pathbuf = {}; char *resolved; size_t len; int ret; @@ -165,6 +168,18 @@ static int program_resolve_path(struct firehose_op *program, const char *program if (program->zip) return 0; + /* Attempt to look up the file in the contents database */ + ret = contents_resolve_path(contents_filter, filename, &pathbuf); + if (ret == 1) { + resolved = strdup(qdl_pathbuf_str(&pathbuf)); + if (!resolved) + return -1; + + free((void *)program->filename); + program->filename = resolved; + return 0; + } + /* Look for the file in include directory */ if (incdir) { snprintf(candidate, sizeof(candidate), "%s/%s", incdir, filename); @@ -202,7 +217,7 @@ static int program_resolve_path(struct firehose_op *program, const char *program static int load_program_tag(struct list_head *ops, xmlNode *node, bool is_nand, bool allow_missing, struct qdl_zip *zip, - const char *program_file, const char *incdir) + const char *program_file, struct contents_filter *contents_filter, const char *incdir) { struct firehose_op *program; struct qdl_file file = {}; @@ -239,7 +254,7 @@ static int load_program_tag(struct list_head *ops, xmlNode *node, bool is_nand, } if (program->filename) { - ret = program_resolve_path(program, program_file, incdir); + ret = program_resolve_path(program, program_file, contents_filter, incdir); if (ret < 0) goto err_free_op; @@ -288,7 +303,7 @@ static int load_program_tag(struct list_head *ops, xmlNode *node, bool is_nand, } int program_load_xml(struct list_head *ops, xmlDoc *doc, struct qdl_zip *zip, const char *program_file, - bool is_nand, bool allow_missing, const char *incdir) + bool is_nand, bool allow_missing, struct contents_filter *contents_filter, const char *incdir) { xmlNode *node; xmlNode *root; @@ -303,7 +318,7 @@ int program_load_xml(struct list_head *ops, xmlDoc *doc, struct qdl_zip *zip, co errors = load_erase_tag(ops, node, is_nand); else if (!xmlStrcmp(node->name, (xmlChar *)"program")) errors = load_program_tag(ops, node, is_nand, allow_missing, zip, - program_file, incdir); + program_file, contents_filter, incdir); else { ux_err("unrecognized tag \"%s\" in program-type file \"%s\"\n", node->name, program_file); errors = -EINVAL; @@ -316,7 +331,8 @@ int program_load_xml(struct list_head *ops, xmlDoc *doc, struct qdl_zip *zip, co return errors; } -int program_load(struct list_head *ops, const char *program_file, bool is_nand, bool allow_missing, const char *incdir) +int program_load(struct list_head *ops, const char *program_file, bool is_nand, + bool allow_missing, struct contents_filter *contents_filter, const char *incdir) { xmlDoc *doc; int errors; @@ -327,7 +343,7 @@ int program_load(struct list_head *ops, const char *program_file, bool is_nand, return -EINVAL; } - errors = program_load_xml(ops, doc, NULL, program_file, is_nand, allow_missing, incdir); + errors = program_load_xml(ops, doc, NULL, program_file, is_nand, allow_missing, contents_filter, incdir); xmlFreeDoc(doc); diff --git a/program.h b/program.h index e528480..35adac6 100644 --- a/program.h +++ b/program.h @@ -11,11 +11,12 @@ struct qdl_device; struct firehose_op; struct qdl_zip; +struct contents_filter; int program_load(struct list_head *ops, const char *program_file, bool is_nand, - bool allow_missing, const char *incdir); + bool allow_missing, struct contents_filter *contents_filter, const char *incdir); int program_load_xml(struct list_head *ops, xmlDoc *doc, struct qdl_zip *zip, const char *program_file, - bool is_nand, bool allow_missing, const char *incdir); + bool is_nand, bool allow_missing, struct contents_filter *contents_filter, const char *incdir); int erase_execute(struct qdl_device *qdl, struct firehose_op *op, int (*apply)(struct qdl_device *qdl, struct firehose_op *op)); int program_find_bootable_partition(struct list_head *ops, bool *multiple_found); diff --git a/qdl.c b/qdl.c index 48e4784..384dd82 100644 --- a/qdl.c +++ b/qdl.c @@ -17,9 +17,12 @@ #include #include "qdl.h" +#include "contents.h" +#include "file.h" #include "firehose.h" #include "flashmap.h" #include "patch.h" +#include "pathbuf.h" #include "program.h" #include "ufs.h" #include "oscompat.h" @@ -102,23 +105,6 @@ static int detect_type(const char *verb) return type; } -enum qdl_storage_type decode_storage(const char *storage) -{ - - if (!strcmp(storage, "emmc")) - return QDL_STORAGE_EMMC; - if (!strcmp(storage, "nand")) - return QDL_STORAGE_NAND; - if (!strcmp(storage, "nvme")) - return QDL_STORAGE_NVME; - if (!strcmp(storage, "spinor")) - return QDL_STORAGE_SPINOR; - if (!strcmp(storage, "ufs")) - return QDL_STORAGE_UFS; - - return QDL_STORAGE_UNKNOWN; -} - #define CPIO_MAGIC "070701" struct cpio_newc_header { char c_magic[6]; /* "070701" */ @@ -266,9 +252,11 @@ static int decode_programmer_archive(struct sahara_image *blob, struct sahara_im * * Returns: 0 if no archive was found, 1 if archive was decoded, -1 on error */ -static int decode_sahara_config(struct sahara_image *blob, struct sahara_image *images) +int decode_sahara_config(struct sahara_image *blob, struct sahara_image *images, + struct contents_filter *contents_filter) { char image_path_full[PATH_MAX]; + struct pathbuf image_full_path = {}; const char *image_path; unsigned int image_id; size_t image_path_len; @@ -328,7 +316,9 @@ static int decode_sahara_config(struct sahara_image *blob, struct sahara_image * image_path_len = strlen(image_path); - if (path_is_absolute(image_path)) { + if (contents_resolve_path(contents_filter, image_path, &image_full_path) == 1) { + memcpy(image_path_full, image_full_path.buf, image_full_path.len + 1); + } else if (path_is_absolute(image_path)) { if (image_path_len + 1 > PATH_MAX) { free((void *)image_path); goto err_free_doc; @@ -429,7 +419,7 @@ static int decode_programmer(char *s, struct sahara_image *images) if (ret < 0 || ret == 1) return ret; - ret = decode_sahara_config(&archive, images); + ret = decode_sahara_config(&archive, images, NULL); if (ret < 0 || ret == 1) return ret; @@ -448,6 +438,8 @@ static void print_usage(FILE *out) fprintf(out, " %s [options] (erase
)...\n", __progname); fprintf(out, " %s list\n", __progname); fprintf(out, " %s ramdump [--debug] [-o ] [,...]\n", __progname); + fprintf(out, " %s flash ([::specifier] | [::])\n", __progname); + fprintf(out, " %s create-zip [::]\n", __progname); fprintf(out, " -d, --debug\t\t\tPrint detailed debug info\n"); fprintf(out, " -v, --version\t\t\tPrint the current version and exit\n"); fprintf(out, " -n, --dry-run\t\t\tDry run execution, no device reading or flashing\n"); @@ -470,8 +462,12 @@ static void print_usage(FILE *out) fprintf(out, " \t\tnumber S, the number of sectors to follow L, or partition by \"name\"\n"); fprintf(out, " \t\tpath where ramdump should stored\n"); fprintf(out, " \toptional glob-pattern to select which segments to ramdump\n"); + fprintf(out, " \tflashmap JSON file, or ZIP archive with flashmap.json\n"); + fprintf(out, " \tcontents XML file\n"); + fprintf(out, " \tcomma-separated list of specifiers, such as storage type and flavors\n"); fprintf(out, "\n"); fprintf(out, "Example: %s prog_firehose_ddr.elf rawprogram*.xml patch*.xml\n", __progname); + fprintf(out, " %s flash contents.xml::ufs,spinor/safe_rtos\n", __progname); } static int qdl_list(FILE *out) @@ -596,72 +592,138 @@ static int qdl_ensure_configured(struct list_head *ops, enum qdl_storage_type st return 0; } -static int qdl_cmd_flash(struct list_head *firehose_ops, char *param, - const char *incdir, struct sahara_image *images) +static char *qdl_split_specifier(const char *param, char **specifier) { - enum qdl_storage_type current_type = QDL_STORAGE_UNKNOWN; - struct list_head flashmap_ops = LIST_INIT(flashmap_ops); - enum qdl_storage_type type; - unsigned int type_filter = 0; - struct firehose_op *next; - struct firehose_op *op; char *filename; - char *filter; - char *save; char *tmp; - int ret; + + if (!param || !param[0]) + return NULL; filename = strdup(param); if (!filename) { ux_err("internal error: unable to allocate memory for argument\n"); - return -1; + return NULL; } + *specifier = NULL; + tmp = strstr(filename, "::"); if (tmp) { - *tmp = '\0'; - filter = tmp + 2; - - for (tmp = strtok_r(filter, ",", &save); tmp; tmp = strtok_r(NULL, ",", &save)) { - type = decode_storage(tmp); - if (type == QDL_STORAGE_UNKNOWN) { - ux_err("unknown storage type \"%s\"\n", tmp); - ret = -1; - goto out_free_filename; - } + if (strstr(tmp + 2, "::")) { + free(filename); + return NULL; + } - type_filter |= 1 << type; + *tmp = '\0'; + if (!filename[0] || !tmp[2]) { + free(filename); + return NULL; } + + *specifier = tmp + 2; } - if (!type_filter) - type_filter = ~0U; + return filename; +} - ret = flashmap_load(&flashmap_ops, filename, images, incdir); - if (ret < 0) - goto out_free_filename; +static int qdl_cmd_flash(struct list_head *firehose_ops, const char *arg, + const char *incdir, struct sahara_image *images) +{ + struct qdl_file flashmap; + struct qdl_zip *zip = NULL; + const char *dot; + char *specifier; + char *filename; + char *tmp; + char *base; + int file_type = QDL_FILE_UNKNOWN; + int ret; + + filename = qdl_split_specifier(arg, &specifier); + if (!filename) { + ux_err("failed to parse flash argument \"%s\" (expected or ::)\n", + arg); + return -1; + } + + tmp = strdup(filename); + if (!tmp) + return -1; - list_for_each_entry_safe(op, next, &flashmap_ops, node) { - if (op->storage_type != QDL_STORAGE_UNKNOWN) - current_type = op->storage_type; + base = basename(tmp); + dot = strrchr(base, '.'); - if ((1 << current_type) & type_filter) { - list_del(&op->node); - list_append(firehose_ops, &op->node); + if (dot && !strcmp(dot, ".xml")) { + file_type = QDL_FILE_CONTENTS; + } else if (dot && !strcmp(dot, ".json")) { + file_type = QDL_CMD_FLASH; + } else { + ret = qdl_zip_open(filename, &zip); + if (!ret) { + ret = qdl_file_open(zip, "flashmap.json", &flashmap); + if (!ret) { + qdl_file_close(&flashmap); + file_type = QDL_CMD_FLASH; + } + qdl_zip_put(zip); } } + free(tmp); + + switch (file_type) { + case QDL_FILE_CONTENTS: + ret = contents_load(firehose_ops, filename, specifier, images, incdir); + break; + case QDL_CMD_FLASH: + ret = flashmap_load(firehose_ops, filename, specifier, images, incdir); + break; + default: + ux_err("flash input must be contents.xml, flashmap.json, or a zip containing flashmap.json\n"); + ret = -1; + break; + } - firehose_free_ops(&flashmap_ops); + free(filename); - if (list_empty(firehose_ops)) { - ux_err("loaded flashmap does not contain any operations for selected storage type\n"); - ret = -1; + return ret; +} + +static int qdl_create_zip(int argc, char **argv) +{ + struct sahara_image images[MAPPING_SZ] = {}; + struct list_head ops = LIST_INIT(ops); + const char *zipfile = argv[1]; + char *specifier; + char *filename; + int ret; + + if (argc != 3) { + print_usage(stderr); + return 1; + } + + ux_init(); + + filename = qdl_split_specifier(argv[2], &specifier); + if (!filename) { + ux_err("failed to parse flash argument"); + return 1; } + ret = contents_load(&ops, filename, specifier, images, NULL); + if (ret < 0) + goto out_free_filename; + + ret = zipper_write(zipfile, &ops, images); + + sahara_images_free(images, MAPPING_SZ); + firehose_free_ops(&ops); + out_free_filename: free(filename); - return ret; + return ret ? 1 : 0; } static int qdl_determine_bootable(struct list_head *ops) @@ -763,7 +825,7 @@ static int qdl_flash(int argc, char **argv) out_chunk_size = strtol(optarg, NULL, 10); break; case 's': - storage_type = decode_storage(optarg); + storage_type = decode_storage_type(optarg); if (storage_type == QDL_STORAGE_UNKNOWN) errx(1, "unknown storage type \"%s\"", optarg); break; @@ -848,7 +910,7 @@ static int qdl_flash(int argc, char **argv) case QDL_FILE_PROGRAM: ret = program_load(&firehose_ops, argv[optind], storage_type == QDL_STORAGE_NAND, - allow_missing, incdir); + allow_missing, NULL, incdir); if (ret < 0) errx(1, "program_load %s failed", argv[optind]); @@ -976,6 +1038,8 @@ int main(int argc, char **argv) return qdl_list(stdout); if (!strcmp(argv[i], "ramdump")) return qdl_ramdump(argc - i, argv + i); + if (!strcmp(argv[i], "create-zip")) + return qdl_create_zip(argc - i, argv + i); if (argv[i][0] != '-') break; } diff --git a/qdl.h b/qdl.h index a692557..863bff2 100644 --- a/qdl.h +++ b/qdl.h @@ -133,7 +133,10 @@ int parse_storage_address(const char *address, int *physical_partition, unsigned int *start_sector, unsigned int *num_sectors, char **gpt_partition); -enum qdl_storage_type decode_storage(const char *storage); +enum qdl_storage_type decode_storage_type(const char *storage); +const char *encode_storage_type(enum qdl_storage_type storage); +int decode_sahara_config(struct sahara_image *blob, struct sahara_image *images, + struct contents_filter *contents_filter); extern bool qdl_debug; diff --git a/tests/common.c b/tests/common.c new file mode 100644 index 0000000..ddf430e --- /dev/null +++ b/tests/common.c @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: BSD-3-Clause +#include +#include +#include + +#include "common.h" + +int test_make_temp_dir(char *buf, size_t size, const char *prefix) +{ + int ret; + +#ifdef _WIN32 + const char *tmp_base = getenv("TMPDIR"); + + if (!tmp_base || tmp_base[0] == '\0') + tmp_base = getenv("TEMP"); + if (!tmp_base || tmp_base[0] == '\0') + tmp_base = getenv("TMP"); + if (!tmp_base || tmp_base[0] == '\0') + tmp_base = "."; + + ret = snprintf(buf, size, "%s/%s-XXXXXX", tmp_base, prefix); +#else + ret = snprintf(buf, size, "/tmp/%s-XXXXXX", prefix); +#endif + + if (ret <= 0 || (size_t)ret >= size) + return -1; + + return mkdtemp(buf) ? 0 : -1; +} diff --git a/tests/common.h b/tests/common.h new file mode 100644 index 0000000..4f823ce --- /dev/null +++ b/tests/common.h @@ -0,0 +1,9 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ +#ifndef __TESTS_COMMON_H__ +#define __TESTS_COMMON_H__ + +#include + +int test_make_temp_dir(char *buf, size_t size, const char *prefix); + +#endif diff --git a/tests/test_contents_selectors.c b/tests/test_contents_selectors.c new file mode 100644 index 0000000..13aa772 --- /dev/null +++ b/tests/test_contents_selectors.c @@ -0,0 +1,415 @@ +// SPDX-License-Identifier: BSD-3-Clause +/* + * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. + */ +#define _FILE_OFFSET_BITS 64 +#define _XOPEN_SOURCE 700 + +#include +#include +#include +#include +#include +#include + +#include + +#include "common.h" +#include "../contents.c" + +bool qdl_debug; + +void ux_err(const char *fmt, ...) +{ + (void)fmt; +} + +void ux_info(const char *fmt, ...) +{ + (void)fmt; +} + +void ux_debug(const char *fmt, ...) +{ + (void)fmt; +} + +enum qdl_storage_type decode_storage_type(const char *storage) +{ + if (!strcmp(storage, "emmc")) + return QDL_STORAGE_EMMC; + if (!strcmp(storage, "nand")) + return QDL_STORAGE_NAND; + if (!strcmp(storage, "ufs")) + return QDL_STORAGE_UFS; + if (!strcmp(storage, "nvme")) + return QDL_STORAGE_NVME; + if (!strcmp(storage, "spinor")) + return QDL_STORAGE_SPINOR; + + return QDL_STORAGE_UNKNOWN; +} + +const char *encode_storage_type(enum qdl_storage_type storage) +{ + switch (storage) { + case QDL_STORAGE_EMMC: + return "emmc"; + case QDL_STORAGE_NAND: + return "nand"; + case QDL_STORAGE_UFS: + return "ufs"; + case QDL_STORAGE_NVME: + return "nvme"; + case QDL_STORAGE_SPINOR: + return "spinor"; + case QDL_STORAGE_UNKNOWN: + default: + return NULL; + } +} + +int load_sahara_image(struct qdl_zip *zip, const char *filename, struct sahara_image *image) +{ + (void)zip; + (void)filename; + (void)image; + + return -1; +} + +void sahara_images_free(struct sahara_image *images, size_t count) +{ + (void)images; + (void)count; +} + +int decode_sahara_config(struct sahara_image *blob, struct sahara_image *images, + struct contents_filter *contents_filter) +{ + (void)blob; + (void)images; + (void)contents_filter; + + return -1; +} + +int program_load(struct list_head *ops, const char *program_file, bool is_nand, + bool allow_missing, struct contents_filter *contents_filter, + const char *incdir) +{ + (void)ops; + (void)program_file; + (void)is_nand; + (void)allow_missing; + (void)contents_filter; + (void)incdir; + + return -1; +} + +int patch_load(struct list_head *ops, const char *patch_file) +{ + (void)ops; + (void)patch_file; + + return -1; +} + +struct selector_fixture { + struct contents contents; + char *flavors[3]; +}; + +static void add_entry(struct contents *contents, enum contents_file_type file_type, + enum qdl_storage_type storage_type, const char *flavor) +{ + struct contents_entry *entry; + + entry = calloc(1, sizeof(*entry)); + assert_non_null(entry); + + entry->file_type = file_type; + entry->storage_type = storage_type; + entry->flavor = flavor ? strdup(flavor) : NULL; + assert_true(!flavor || entry->flavor); + + list_append(&contents->entries, &entry->node); +} + +static int setup_single(void **state) +{ + struct selector_fixture *fixture; + + fixture = calloc(1, sizeof(*fixture)); + assert_non_null(fixture); + + fixture->flavors[0] = "flavor_a"; + + list_init(&fixture->contents.entries); + fixture->contents.flavors = fixture->flavors; + fixture->contents.num_flavors = 1; + + add_entry(&fixture->contents, CONTENTS_FILE_PROGRAM, QDL_STORAGE_UFS, "flavor_a"); + add_entry(&fixture->contents, CONTENTS_FILE_PROGRAM, QDL_STORAGE_UFS, NULL); + add_entry(&fixture->contents, CONTENTS_FILE_PATCH, QDL_STORAGE_UFS, NULL); + add_entry(&fixture->contents, CONTENTS_FILE_OTHER, QDL_STORAGE_UNKNOWN, "flavor_a"); + + *state = fixture; + return 0; +} + +static int setup_multi(void **state) +{ + struct selector_fixture *fixture; + + fixture = calloc(1, sizeof(*fixture)); + assert_non_null(fixture); + + fixture->flavors[0] = "flavor_a"; + fixture->flavors[1] = "flavor_b"; + fixture->flavors[2] = "flavor_c"; + + list_init(&fixture->contents.entries); + fixture->contents.flavors = fixture->flavors; + fixture->contents.num_flavors = 3; + + add_entry(&fixture->contents, CONTENTS_FILE_PROGRAM, QDL_STORAGE_UFS, "flavor_a"); + add_entry(&fixture->contents, CONTENTS_FILE_PATCH, QDL_STORAGE_UFS, "flavor_a"); + add_entry(&fixture->contents, CONTENTS_FILE_PROGRAM, QDL_STORAGE_SPINOR, "flavor_b"); + add_entry(&fixture->contents, CONTENTS_FILE_PATCH, QDL_STORAGE_SPINOR, "flavor_b"); + add_entry(&fixture->contents, CONTENTS_FILE_PROGRAM, QDL_STORAGE_SPINOR, "flavor_c"); + add_entry(&fixture->contents, CONTENTS_FILE_PATCH, QDL_STORAGE_SPINOR, "flavor_c"); + add_entry(&fixture->contents, CONTENTS_FILE_OTHER, QDL_STORAGE_UNKNOWN, "flavor_a"); + + *state = fixture; + return 0; +} + +static int teardown_fixture(void **state) +{ + struct selector_fixture *fixture = *state; + struct contents_entry *entry; + struct contents_entry *next; + + if (!fixture) + return 0; + + list_for_each_entry_safe(entry, next, &fixture->contents.entries, node) { + list_del(&entry->node); + free(entry->flavor); + free(entry); + } + + free(fixture); + return 0; +} + +static void assert_selector(struct contents_selector *selector, + enum qdl_storage_type storage_type, const char *flavor) +{ + assert_int_equal(selector->storage_type, storage_type); + assert_string_equal(selector->flavor, flavor); +} + +static void assert_decode_success(struct contents *contents, const char *pattern, + int expected_count, + const struct contents_selector *expected) +{ + struct contents_selector *selectors = NULL; + char *pattern_copy = NULL; + int ret; + int i; + + if (pattern) { + pattern_copy = strdup(pattern); + assert_non_null(pattern_copy); + } + + ret = contents_decode_selectors(contents, pattern_copy, &selectors); + assert_int_equal(ret, expected_count); + assert_non_null(selectors); + + for (i = 0; i < expected_count; i++) + assert_selector(&selectors[i], expected[i].storage_type, expected[i].flavor); + + free(selectors); + free(pattern_copy); +} + +static void assert_decode_failure(struct contents *contents, const char *pattern) +{ + struct contents_selector *selectors = NULL; + char *pattern_copy = NULL; + int ret; + + if (pattern) { + pattern_copy = strdup(pattern); + assert_non_null(pattern_copy); + } + + ret = contents_decode_selectors(contents, pattern_copy, &selectors); + assert_int_equal(ret, -1); + assert_null(selectors); + + free(pattern_copy); +} + +static void test_single_autoselects_only_combination(void **state) +{ + struct selector_fixture *fixture = *state; + const struct contents_selector expected[] = { + { QDL_STORAGE_UFS, "flavor_a" }, + }; + + assert_decode_success(&fixture->contents, NULL, 1, expected); +} + +static void test_single_accepts_storage_shorthand(void **state) +{ + struct selector_fixture *fixture = *state; + const struct contents_selector expected[] = { + { QDL_STORAGE_UFS, "flavor_a" }, + }; + + assert_decode_success(&fixture->contents, "ufs", 1, expected); +} + +static void test_single_accepts_flavor_shorthand(void **state) +{ + struct selector_fixture *fixture = *state; + const struct contents_selector expected[] = { + { QDL_STORAGE_UFS, "flavor_a" }, + }; + + assert_decode_success(&fixture->contents, "flavor_a", 1, expected); +} + +static void test_multi_requires_explicit_selector(void **state) +{ + struct selector_fixture *fixture = *state; + + assert_decode_failure(&fixture->contents, NULL); +} + +static void test_multi_accepts_full_multi_selector(void **state) +{ + struct selector_fixture *fixture = *state; + const struct contents_selector expected[] = { + { QDL_STORAGE_UFS, "flavor_a" }, + { QDL_STORAGE_SPINOR, "flavor_b" }, + }; + + assert_decode_success(&fixture->contents, + "ufs/flavor_a,spinor/flavor_b", + 2, expected); +} + +static void test_multi_accepts_storage_shorthand_when_unique(void **state) +{ + struct selector_fixture *fixture = *state; + const struct contents_selector expected[] = { + { QDL_STORAGE_UFS, "flavor_a" }, + }; + + assert_decode_success(&fixture->contents, "ufs", 1, expected); +} + +static void test_multi_accepts_flavor_shorthand_when_unique(void **state) +{ + struct selector_fixture *fixture = *state; + const struct contents_selector expected[] = { + { QDL_STORAGE_SPINOR, "flavor_b" }, + }; + + assert_decode_success(&fixture->contents, "flavor_b", 1, expected); +} + +static void test_multi_accepts_mixed_shorthand_selector(void **state) +{ + struct selector_fixture *fixture = *state; + const struct contents_selector expected[] = { + { QDL_STORAGE_UFS, "flavor_a" }, + { QDL_STORAGE_SPINOR, "flavor_b" }, + }; + + assert_decode_success(&fixture->contents, "ufs,spinor/flavor_b", 2, expected); +} + +static void test_multi_rejects_ambiguous_storage_shorthand(void **state) +{ + struct selector_fixture *fixture = *state; + + assert_decode_failure(&fixture->contents, "spinor"); +} + +static void test_multi_rejects_invalid_combination(void **state) +{ + struct selector_fixture *fixture = *state; + + assert_decode_failure(&fixture->contents, "ufs/flavor_b"); +} + +static void test_multi_rejects_duplicate_storage_full_selectors(void **state) +{ + struct selector_fixture *fixture = *state; + + assert_decode_failure(&fixture->contents, "spinor/flavor_b,spinor/flavor_c"); +} + +static void test_multi_rejects_duplicate_storage_after_shorthand(void **state) +{ + struct selector_fixture *fixture = *state; + + assert_decode_failure(&fixture->contents, "flavor_b,spinor/flavor_c"); +} + +static void test_multi_rejects_unknown_selector(void **state) +{ + struct selector_fixture *fixture = *state; + + assert_decode_failure(&fixture->contents, "does_not_exist"); +} + +static void test_multi_rejects_empty_selector(void **state) +{ + struct selector_fixture *fixture = *state; + + assert_decode_failure(&fixture->contents, ""); +} + +int main(void) +{ + const struct CMUnitTest tests[] = { + cmocka_unit_test_setup_teardown(test_single_autoselects_only_combination, + setup_single, teardown_fixture), + cmocka_unit_test_setup_teardown(test_single_accepts_storage_shorthand, + setup_single, teardown_fixture), + cmocka_unit_test_setup_teardown(test_single_accepts_flavor_shorthand, + setup_single, teardown_fixture), + cmocka_unit_test_setup_teardown(test_multi_requires_explicit_selector, + setup_multi, teardown_fixture), + cmocka_unit_test_setup_teardown(test_multi_accepts_full_multi_selector, + setup_multi, teardown_fixture), + cmocka_unit_test_setup_teardown(test_multi_accepts_storage_shorthand_when_unique, + setup_multi, teardown_fixture), + cmocka_unit_test_setup_teardown(test_multi_accepts_flavor_shorthand_when_unique, + setup_multi, teardown_fixture), + cmocka_unit_test_setup_teardown(test_multi_accepts_mixed_shorthand_selector, + setup_multi, teardown_fixture), + cmocka_unit_test_setup_teardown(test_multi_rejects_ambiguous_storage_shorthand, + setup_multi, teardown_fixture), + cmocka_unit_test_setup_teardown(test_multi_rejects_invalid_combination, + setup_multi, teardown_fixture), + cmocka_unit_test_setup_teardown(test_multi_rejects_duplicate_storage_full_selectors, + setup_multi, teardown_fixture), + cmocka_unit_test_setup_teardown(test_multi_rejects_duplicate_storage_after_shorthand, + setup_multi, teardown_fixture), + cmocka_unit_test_setup_teardown(test_multi_rejects_unknown_selector, + setup_multi, teardown_fixture), + cmocka_unit_test_setup_teardown(test_multi_rejects_empty_selector, + setup_multi, teardown_fixture), + }; + + return cmocka_run_group_tests(tests, NULL, NULL); +} diff --git a/tests/test_contents_xml.c b/tests/test_contents_xml.c new file mode 100644 index 0000000..4c4161c --- /dev/null +++ b/tests/test_contents_xml.c @@ -0,0 +1,547 @@ +// SPDX-License-Identifier: BSD-3-Clause +/* + * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. + */ +#define _FILE_OFFSET_BITS 64 +#if defined(__APPLE__) +#define _DARWIN_C_SOURCE +#endif +#define _XOPEN_SOURCE 700 + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +static xmlDocPtr mock_xmlReadFile(const char *filename, const char *encoding, int options); + +#define xmlReadFile mock_xmlReadFile +#include "../contents.c" +#undef xmlReadFile + +bool qdl_debug; +static int mock_load_sahara_ret = -1; +static int mock_decode_sahara_ret = -1; +static unsigned int mock_load_call_count; +static unsigned int mock_decode_call_count; +static char mock_last_load_filename[PATH_MAX]; + +void ux_err(const char *fmt, ...) +{ + (void)fmt; +} + +void ux_info(const char *fmt, ...) +{ + (void)fmt; +} + +void ux_debug(const char *fmt, ...) +{ + (void)fmt; +} + +enum qdl_storage_type decode_storage_type(const char *storage) +{ + if (!strcmp(storage, "emmc")) + return QDL_STORAGE_EMMC; + if (!strcmp(storage, "nand")) + return QDL_STORAGE_NAND; + if (!strcmp(storage, "ufs")) + return QDL_STORAGE_UFS; + if (!strcmp(storage, "nvme")) + return QDL_STORAGE_NVME; + if (!strcmp(storage, "spinor")) + return QDL_STORAGE_SPINOR; + + return QDL_STORAGE_UNKNOWN; +} + +const char *encode_storage_type(enum qdl_storage_type storage) +{ + switch (storage) { + case QDL_STORAGE_EMMC: + return "emmc"; + case QDL_STORAGE_NAND: + return "nand"; + case QDL_STORAGE_UFS: + return "ufs"; + case QDL_STORAGE_NVME: + return "nvme"; + case QDL_STORAGE_SPINOR: + return "spinor"; + case QDL_STORAGE_UNKNOWN: + default: + return NULL; + } +} + +int load_sahara_image(struct qdl_zip *zip, const char *filename, struct sahara_image *image) +{ + (void)zip; + + mock_load_call_count++; + snprintf(mock_last_load_filename, sizeof(mock_last_load_filename), "%s", filename); + + if (image) { + image->name = mock_last_load_filename; + image->ptr = NULL; + image->len = 0; + } + + return mock_load_sahara_ret; +} + +void sahara_images_free(struct sahara_image *images, size_t count) +{ + (void)images; + (void)count; +} + +int decode_sahara_config(struct sahara_image *blob, struct sahara_image *images, + struct contents_filter *contents_filter) +{ + (void)blob; + (void)images; + (void)contents_filter; + + mock_decode_call_count++; + return mock_decode_sahara_ret; +} + +int program_load(struct list_head *ops, const char *program_file, bool is_nand, + bool allow_missing, struct contents_filter *contents_filter, + const char *incdir) +{ + (void)ops; + (void)program_file; + (void)is_nand; + (void)allow_missing; + (void)contents_filter; + (void)incdir; + + return -1; +} + +int patch_load(struct list_head *ops, const char *patch_file) +{ + (void)ops; + (void)patch_file; + + return -1; +} + +struct firehose_op *firehose_alloc_op(int type) +{ + struct firehose_op *op = calloc(1, sizeof(*op)); + + if (!op) + return NULL; + + op->type = type; + return op; +} + +#define TEST_ROOT "/mock" +#define TEST_CONTENTS_XML TEST_ROOT "/contents.xml" +#define TEST_INVALID_XML TEST_ROOT "/not-contents.xml" + +struct xml_fixture { + const char *root; + const char *contents_xml; + const char *invalid_xml; +}; + +static const char contents_xml[] = + "" + "" + " " + " flavor_a" + " flavor_b" + " " + " " + " " + " ./root/" + " " + " programmer.xml" + " common/build/" + " " + " " + " prog_firehose.elf" + " boot/" + " " + " " + " payload.bin" + " images/" + " " + " " + " rawprogram0.xml" + " ufs/${variant:raw}/" + " " + " " + " patch0.xml" + " ufs/" + " " + " " + " spinor_rawprogram.xml" + " spinor/" + " " + " " + " ignored.bin" + " ignored/" + " " + " " + " *.bin" + " wildcard/" + " " + " " + " " + ""; + +static const char contents_xml_device_programmer_only[] = + "" + "" + " " + " " + " ./root/" + " " + " prog_firehose_lite.elf" + " boot/" + " " + " " + " prog_firehose_ddr.elf" + " boot/" + " " + " " + " " + ""; + +static const char contents_xml_device_programmer_lite_only[] = + "" + "" + " " + " " + " ./root/" + " " + " prog_firehose_lite.elf" + " boot/" + " " + " " + " " + ""; + +static const char contents_xml_storage_only[] = + "" + "" + " " + " " + " ./root/" + " " + " rawprogram0.xml" + " common/build/ufs/" + " " + " " + " patch0.xml" + " common/build/ufs/" + " " + " " + " " + ""; + +static const char *mock_contents_xml_text; +static const char *mock_invalid_xml_text; + +static xmlDocPtr mock_xmlReadFile(const char *filename, const char *encoding, int options) +{ + const char *xml; + + (void)encoding; + + if (!strcmp(filename, TEST_CONTENTS_XML)) + xml = mock_contents_xml_text; + else if (!strcmp(filename, TEST_INVALID_XML)) + xml = mock_invalid_xml_text; + else + return NULL; + + return xml ? xmlReadMemory(xml, strlen(xml), filename, NULL, options) : NULL; +} + +static void reset_programmer_mocks(void) +{ + mock_load_sahara_ret = -1; + mock_decode_sahara_ret = -1; + mock_load_call_count = 0; + mock_decode_call_count = 0; + mock_last_load_filename[0] = '\0'; +} + +static int setup_xml_fixture(void **state) +{ + struct xml_fixture *fixture; + + fixture = calloc(1, sizeof(*fixture)); + assert_non_null(fixture); + + fixture->root = TEST_ROOT; + fixture->contents_xml = TEST_CONTENTS_XML; + fixture->invalid_xml = TEST_INVALID_XML; + mock_contents_xml_text = contents_xml; + mock_invalid_xml_text = "\n"; + reset_programmer_mocks(); + + *state = fixture; + return 0; +} + +static int teardown_xml_fixture(void **state) +{ + free(*state); + return 0; +} + +static void init_contents(struct contents *contents) +{ + list_init(&contents->entries); +} + +static void free_contents(struct contents *contents) +{ + struct contents_entry *entry; + struct contents_entry *next; + size_t flavor_idx; + + list_for_each_entry_safe(entry, next, &contents->entries, node) { + list_del(&entry->node); + free(entry->filename); + free(entry->flavor); + free(entry); + } + + for (flavor_idx = 0; flavor_idx < contents->num_flavors; flavor_idx++) + free(contents->flavors[flavor_idx]); + free(contents->flavors); +} + +static size_t count_entries(struct contents *contents) +{ + struct contents_entry *entry; + size_t count = 0; + + list_for_each_entry(entry, &contents->entries, node) + count++; + + return count; +} + +static struct contents_entry *find_entry(struct contents *contents, const char *filename) +{ + struct contents_entry *entry; + + list_for_each_entry(entry, &contents->entries, node) { + if (!strcmp(entry->filename, filename)) + return entry; + } + + return NULL; +} + +static void assert_entry(struct xml_fixture *fixture, struct contents *contents, + const char *filename, enum contents_file_type file_type, + enum qdl_storage_type storage_type, const char *flavor, + enum firehose_type firehose_type, const char *relative_path) +{ + struct contents_entry *entry; + char expected_path[PATH_MAX]; + int ret; + + entry = find_entry(contents, filename); + assert_non_null(entry); + assert_int_equal(entry->file_type, file_type); + assert_int_equal(entry->storage_type, storage_type); + if (flavor) + assert_string_equal(entry->flavor, flavor); + else + assert_null(entry->flavor); + assert_int_equal(entry->firehose_type, firehose_type); + + ret = snprintf(expected_path, sizeof(expected_path), "%s/root/%s", + fixture->root, relative_path); + assert_true(ret > 0 && (size_t)ret < sizeof(expected_path)); + assert_string_equal(qdl_pathbuf_str(&entry->path), expected_path); +} + +static void test_load_xml_parses_product_flavors(void **state) +{ + struct xml_fixture *fixture = *state; + struct contents contents = {}; + + init_contents(&contents); + assert_int_equal(contents_load_xml(&contents, fixture->contents_xml), 0); + assert_int_equal(contents.num_flavors, 2); + assert_string_equal(contents.flavors[0], "flavor_a"); + assert_string_equal(contents.flavors[1], "flavor_b"); + assert_string_equal(qdl_pathbuf_str(&contents.base_dir), fixture->root); + + free_contents(&contents); +} + +static void test_load_xml_parses_file_entries(void **state) +{ + struct xml_fixture *fixture = *state; + struct contents contents = {}; + + init_contents(&contents); + assert_int_equal(contents_load_xml(&contents, fixture->contents_xml), 0); + assert_int_equal(count_entries(&contents), 6); + + assert_entry(fixture, &contents, "programmer.xml", + CONTENTS_FILE_PROGRAMMER_XML, QDL_STORAGE_UNKNOWN, NULL, 2, + "common/build/programmer.xml"); + assert_entry(fixture, &contents, "prog_firehose.elf", + CONTENTS_FILE_DEVICE_PROGRAMMER, QDL_STORAGE_UFS, NULL, + 0, "boot/prog_firehose.elf"); + assert_entry(fixture, &contents, "payload.bin", + CONTENTS_FILE_OTHER, QDL_STORAGE_UFS, "flavor_a", 0, + "images/payload.bin"); + assert_entry(fixture, &contents, "rawprogram0.xml", + CONTENTS_FILE_PROGRAM, QDL_STORAGE_UFS, "flavor_a", 0, + "ufs/raw/rawprogram0.xml"); + assert_entry(fixture, &contents, "patch0.xml", + CONTENTS_FILE_PATCH, QDL_STORAGE_UFS, NULL, 0, + "ufs/patch0.xml"); + assert_entry(fixture, &contents, "spinor_rawprogram.xml", + CONTENTS_FILE_PROGRAM, QDL_STORAGE_SPINOR, "flavor_b", 0, + "spinor/spinor_rawprogram.xml"); + + free_contents(&contents); +} + +static void test_load_xml_skips_ignored_and_wildcards(void **state) +{ + struct xml_fixture *fixture = *state; + struct contents contents = {}; + + init_contents(&contents); + assert_int_equal(contents_load_xml(&contents, fixture->contents_xml), 0); + assert_null(find_entry(&contents, "ignored.bin")); + assert_null(find_entry(&contents, "*.bin")); + + free_contents(&contents); +} + +static void test_load_xml_rejects_non_contents_document(void **state) +{ + struct xml_fixture *fixture = *state; + struct contents contents = {}; + + init_contents(&contents); + assert_int_equal(contents_load_xml(&contents, fixture->invalid_xml), -1); +} + +static void test_find_programmers_prefers_programmer_xml(void **state) +{ + struct xml_fixture *fixture = *state; + struct sahara_image images[MAPPING_SZ] = {}; + struct contents contents = {}; + + mock_load_sahara_ret = 0; + mock_decode_sahara_ret = 1; + + init_contents(&contents); + assert_int_equal(contents_load_xml(&contents, fixture->contents_xml), 0); + assert_int_equal(contents_find_programmers(&contents, images), 0); + assert_int_equal(mock_decode_call_count, 1); + assert_int_equal(mock_load_call_count, 1); + assert_non_null(strstr(mock_last_load_filename, "programmer.xml")); + + free_contents(&contents); +} + +static void test_find_programmers_falls_back_to_non_lite_device_programmer(void **state) +{ + struct xml_fixture *fixture = *state; + struct sahara_image images[MAPPING_SZ] = {}; + struct contents contents = {}; + + mock_contents_xml_text = contents_xml_device_programmer_only; + mock_load_sahara_ret = 0; + + init_contents(&contents); + assert_int_equal(contents_load_xml(&contents, fixture->contents_xml), 0); + assert_int_equal(contents_find_programmers(&contents, images), 0); + assert_int_equal(mock_decode_call_count, 0); + assert_int_equal(mock_load_call_count, 1); + assert_non_null(strstr(mock_last_load_filename, "prog_firehose_ddr.elf")); + + free_contents(&contents); +} + +static void test_find_programmers_rejects_lite_only_device_programmer(void **state) +{ + struct xml_fixture *fixture = *state; + struct sahara_image images[MAPPING_SZ] = {}; + struct contents contents = {}; + + mock_contents_xml_text = contents_xml_device_programmer_lite_only; + mock_load_sahara_ret = 0; + + init_contents(&contents); + assert_int_equal(contents_load_xml(&contents, fixture->contents_xml), 0); + assert_int_equal(contents_find_programmers(&contents, images), -1); + assert_int_equal(mock_decode_call_count, 0); + assert_int_equal(mock_load_call_count, 0); + + free_contents(&contents); +} + +static void test_decode_selectors_accepts_storage_only_contents(void **state) +{ + struct xml_fixture *fixture = *state; + struct contents_selector *selectors = NULL; + struct contents contents = {}; + char pattern[] = "ufs"; + + mock_contents_xml_text = contents_xml_storage_only; + + init_contents(&contents); + assert_int_equal(contents_load_xml(&contents, fixture->contents_xml), 0); + assert_int_equal(contents_decode_selectors(&contents, pattern, &selectors), 1); + assert_non_null(selectors); + assert_int_equal(selectors[0].storage_type, QDL_STORAGE_UFS); + assert_null(selectors[0].flavor); + + free(selectors); + free_contents(&contents); +} + +int main(void) +{ + const struct CMUnitTest tests[] = { + cmocka_unit_test_setup_teardown(test_load_xml_parses_product_flavors, + setup_xml_fixture, teardown_xml_fixture), + cmocka_unit_test_setup_teardown(test_load_xml_parses_file_entries, + setup_xml_fixture, teardown_xml_fixture), + cmocka_unit_test_setup_teardown(test_load_xml_skips_ignored_and_wildcards, + setup_xml_fixture, teardown_xml_fixture), + cmocka_unit_test_setup_teardown(test_load_xml_rejects_non_contents_document, + setup_xml_fixture, teardown_xml_fixture), + cmocka_unit_test_setup_teardown(test_find_programmers_prefers_programmer_xml, + setup_xml_fixture, teardown_xml_fixture), + cmocka_unit_test_setup_teardown(test_find_programmers_falls_back_to_non_lite_device_programmer, + setup_xml_fixture, teardown_xml_fixture), + cmocka_unit_test_setup_teardown(test_find_programmers_rejects_lite_only_device_programmer, + setup_xml_fixture, teardown_xml_fixture), + cmocka_unit_test_setup_teardown(test_decode_selectors_accepts_storage_only_contents, + setup_xml_fixture, teardown_xml_fixture), + }; + + return cmocka_run_group_tests(tests, NULL, NULL); +} diff --git a/tests/test_pathbuf.c b/tests/test_pathbuf.c new file mode 100644 index 0000000..f1ba99c --- /dev/null +++ b/tests/test_pathbuf.c @@ -0,0 +1,184 @@ +// SPDX-License-Identifier: BSD-3-Clause +#define _FILE_OFFSET_BITS 64 + +#include +#include +#include +#include +#include +#include + +#include + +#include "pathbuf.h" + +static void init_path(struct pathbuf *path, const char *s, bool set_len) +{ + size_t len = strlen(s); + + assert_true(len < sizeof(path->buf)); + memcpy(path->buf, s, len + 1); + path->len = set_len ? len : 0; +} + +static void assert_dirname(const char *in, const char *out, bool set_len) +{ + struct pathbuf path; + + init_path(&path, in, set_len); + qdl_pathbuf_dirname(&path); + + assert_string_equal(path.buf, out); + assert_int_equal(path.len, strlen(out)); +} + +static void test_dirname_posix_root(void **state) +{ + (void)state; + assert_dirname("/", "/", true); + assert_dirname("/", "/", false); +} + +static void test_dirname_posix_absolute(void **state) +{ + (void)state; + assert_dirname("/foo", "/", true); + assert_dirname("/foo/bar", "/foo", true); + assert_dirname("/foo/bar/", "/foo", true); +} + +static void test_dirname_relative(void **state) +{ + (void)state; + assert_dirname("foo", "foo", true); + assert_dirname("foo/bar", "foo", true); + assert_dirname("foo/bar/", "foo", true); +} + +static void test_reset_and_dup(void **state) +{ + struct pathbuf src; + struct pathbuf dst; + + (void)state; + + init_path(&src, "/tmp/example", true); + qdl_pathbuf_dup(&dst, &src); + assert_string_equal(dst.buf, src.buf); + assert_int_equal(dst.len, src.len); + + qdl_pathbuf_reset(&dst); + assert_string_equal(dst.buf, ""); + assert_int_equal(dst.len, 0); + + qdl_pathbuf_reset(NULL); +} + +static void test_push_basic_behavior(void **state) +{ + struct pathbuf path = {0}; + int ret; + + (void)state; + + ret = qdl_pathbuf_push(&path, "foo"); + assert_int_equal(ret, 0); + assert_string_equal(path.buf, "foo"); + assert_int_equal(path.len, strlen("foo")); + + ret = qdl_pathbuf_push(&path, "bar"); + assert_int_equal(ret, 0); + assert_string_equal(path.buf, "foo/bar"); + assert_int_equal(path.len, strlen("foo/bar")); + + ret = qdl_pathbuf_push(&path, ""); + assert_int_equal(ret, 0); + assert_string_equal(path.buf, "foo/bar"); + assert_int_equal(path.len, strlen("foo/bar")); + + ret = qdl_pathbuf_push(&path, "./baz"); + assert_int_equal(ret, 0); + assert_string_equal(path.buf, "foo/bar/baz"); + assert_int_equal(path.len, strlen("foo/bar/baz")); + +#ifdef _WIN32 + ret = qdl_pathbuf_push(&path, "C:/abs"); + assert_int_equal(ret, 0); + assert_string_equal(path.buf, "C:/abs"); + assert_int_equal(path.len, strlen("C:/abs")); +#else + ret = qdl_pathbuf_push(&path, "/abs"); + assert_int_equal(ret, 0); + assert_string_equal(path.buf, "/abs"); + assert_int_equal(path.len, strlen("/abs")); +#endif +} + +static void test_push_invalid_args(void **state) +{ + struct pathbuf path = {0}; + + (void)state; + + assert_int_equal(qdl_pathbuf_push(NULL, "foo"), -EINVAL); + assert_int_equal(qdl_pathbuf_push(&path, NULL), -EINVAL); +} + +static void test_push_overflow_keeps_state(void **state) +{ + struct pathbuf path = {0}; + char before[PATH_MAX]; + size_t max_prefix_len; + int ret; + + (void)state; + + max_prefix_len = sizeof(path.buf) - 1; + memset(path.buf, 'a', max_prefix_len); + path.buf[max_prefix_len] = '\0'; + path.len = max_prefix_len; + memcpy(before, path.buf, sizeof(before)); + + ret = qdl_pathbuf_push(&path, "b"); + assert_int_equal(ret, -ENAMETOOLONG); + assert_string_equal(path.buf, before); + assert_int_equal(path.len, max_prefix_len); +} + +#ifdef _WIN32 +static void test_dirname_windows_drive(void **state) +{ + (void)state; + assert_dirname("C:\\", "C:\\", true); + assert_dirname("C:\\foo", "C:\\", true); + assert_dirname("C:\\foo\\bar", "C:\\foo", true); + assert_dirname("C:/foo/bar", "C:/foo", true); +} + +static void test_dirname_windows_unc(void **state) +{ + (void)state; + assert_dirname("\\\\server\\share", "\\\\server\\share", true); + assert_dirname("\\\\server\\share\\dir", "\\\\server\\share", true); + assert_dirname("\\\\server\\share\\dir\\sub", "\\\\server\\share\\dir", true); +} +#endif + +int main(void) +{ + const struct CMUnitTest tests[] = { + cmocka_unit_test(test_dirname_posix_root), + cmocka_unit_test(test_dirname_posix_absolute), + cmocka_unit_test(test_dirname_relative), + cmocka_unit_test(test_reset_and_dup), + cmocka_unit_test(test_push_basic_behavior), + cmocka_unit_test(test_push_invalid_args), + cmocka_unit_test(test_push_overflow_keeps_state), +#ifdef _WIN32 + cmocka_unit_test(test_dirname_windows_drive), + cmocka_unit_test(test_dirname_windows_unc), +#endif + }; + + return cmocka_run_group_tests(tests, NULL, NULL); +} diff --git a/tests/test_program_load_xml.c b/tests/test_program_load_xml.c index 8bb724f..4df5b54 100644 --- a/tests/test_program_load_xml.c +++ b/tests/test_program_load_xml.c @@ -25,9 +25,11 @@ #include "file.h" #include "firehose.h" #include "list.h" +#include "pathbuf.h" #include "program.h" #include "qdl.h" #include "sparse.h" +#include "common.h" #define TEST_INCDIR "/mock/incdir" #define TEST_XMLDIR "/mock/xml" @@ -211,6 +213,15 @@ void firehose_free_ops(struct list_head *ops) } } +int contents_resolve_path(struct contents_filter *filter, const char *filename, struct pathbuf *path) +{ + (void)filter; + (void)filename; + (void)path; + + return 0; +} + static xmlDoc *build_program_doc(const char *filename) { char xml[1024]; @@ -267,7 +278,7 @@ static void test_incdir_takes_precedence(void **state) if (!doc) fail_msg("failed to parse synthetic program XML"); - ret = program_load_xml(&ops, doc, NULL, TEST_PROGRAM_FILE, false, false, TEST_INCDIR); + ret = program_load_xml(&ops, doc, NULL, TEST_PROGRAM_FILE, false, false, NULL, TEST_INCDIR); if (ret) fail_msg("incdir payload should be accepted, got %d", ret); @@ -294,7 +305,7 @@ static void test_program_xml_directory_is_used(void **state) if (!doc) fail_msg("failed to parse synthetic program XML"); - ret = program_load_xml(&ops, doc, NULL, TEST_PROGRAM_FILE, false, false, TEST_INCDIR); + ret = program_load_xml(&ops, doc, NULL, TEST_PROGRAM_FILE, false, false, NULL, TEST_INCDIR); if (ret) fail_msg("XML-adjacent payload should be accepted, got %d", ret); @@ -321,7 +332,7 @@ static void test_program_xml_directory_takes_precedence_over_current_directory(v if (!doc) fail_msg("failed to parse synthetic program XML"); - ret = program_load_xml(&ops, doc, NULL, TEST_PROGRAM_FILE, false, false, TEST_INCDIR); + ret = program_load_xml(&ops, doc, NULL, TEST_PROGRAM_FILE, false, false, NULL, TEST_INCDIR); if (ret) fail_msg("XML-adjacent payload should beat current-directory fallback, got %d", ret); @@ -349,7 +360,7 @@ static void test_current_directory_is_used_when_path_resolution_misses(void **st fail_msg("failed to parse synthetic program XML"); /* Compatibility fallback: leave the original relative filename intact. */ - ret = program_load_xml(&ops, doc, NULL, TEST_PROGRAM_FILE, false, false, TEST_INCDIR); + ret = program_load_xml(&ops, doc, NULL, TEST_PROGRAM_FILE, false, false, NULL, TEST_INCDIR); if (ret) fail_msg("relative fallback payload should be accepted, got %d", ret); @@ -375,7 +386,7 @@ static void test_missing_file_fails_without_allow_missing(void **state) if (!doc) fail_msg("failed to parse synthetic program XML"); - ret = program_load_xml(&ops, doc, NULL, TEST_PROGRAM_FILE, false, false, TEST_INCDIR); + ret = program_load_xml(&ops, doc, NULL, TEST_PROGRAM_FILE, false, false, NULL, TEST_INCDIR); if (ret != -1) fail_msg("missing payload should fail without allow_missing, got %d", ret); if (!list_empty(&ops)) @@ -398,7 +409,7 @@ static void test_missing_file_is_tolerated_with_allow_missing(void **state) if (!doc) fail_msg("failed to parse synthetic program XML"); - ret = program_load_xml(&ops, doc, NULL, TEST_PROGRAM_FILE, false, true, TEST_INCDIR); + ret = program_load_xml(&ops, doc, NULL, TEST_PROGRAM_FILE, false, true, NULL, TEST_INCDIR); if (ret) fail_msg("missing payload should be tolerated with allow_missing, got %d", ret); diff --git a/util.c b/util.c index 9e891ea..026588d 100644 --- a/util.c +++ b/util.c @@ -124,6 +124,36 @@ bool attr_as_bool(xmlNode *node, const char *attr, int *errors) return ret; } +static const char * const storage_types[] = { + [QDL_STORAGE_EMMC] = "emmc", + [QDL_STORAGE_NAND] = "nand", + [QDL_STORAGE_NVME] = "nvme", + [QDL_STORAGE_SPINOR] = "spinor", + [QDL_STORAGE_UFS] = "ufs", +}; + +const char *encode_storage_type(enum qdl_storage_type storage) +{ + if ((unsigned int)storage >= ARRAY_SIZE(storage_types)) + return NULL; + + return storage_types[storage]; +} + +enum qdl_storage_type decode_storage_type(const char *storage) +{ + unsigned int i; + + if (!storage) + return QDL_STORAGE_UNKNOWN; + + for (i = 0; i < ARRAY_SIZE(storage_types); i++) + if (storage_types[i] && !strcmp(storage, storage_types[i])) + return i; + + return QDL_STORAGE_UNKNOWN; +} + /*** * parse_storage_address() - parse a storage address specifier * @address: specifier to be parsed diff --git a/zipper.c b/zipper.c new file mode 100644 index 0000000..2754caf --- /dev/null +++ b/zipper.c @@ -0,0 +1,1068 @@ +// SPDX-License-Identifier: BSD-3-Clause +/* + * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. + */ +#define _GNU_SOURCE +#define _FILE_OFFSET_BITS 64 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "flashmap.h" +#include "firehose.h" +#include "list.h" +#include "qdl.h" +#include "sparse.h" + +static char *zipper_basename(const char *path) +{ + char *tmp; + char *base; + + if (!path || !path[0]) + return NULL; + + tmp = strdup(path); + if (!tmp) + return NULL; + + base = basename(tmp); + base = base ? strdup(base) : NULL; + free(tmp); + + return base; +} + +static void xml_setpropf(xmlNode *node, const char *attr, const char *fmt, ...) +{ + char *buf; + va_list ap; + + if (!node || !attr || !fmt) + return; + + va_start(ap, fmt); + if (vasprintf(&buf, fmt, ap) < 0) { + va_end(ap); + return; + } + va_end(ap); + + xmlSetProp(node, (xmlChar *)attr, (xmlChar *)buf); + free(buf); +} + +struct zipper_name_entry { + char *name; + struct list_head node; +}; + +struct zipper_file_rename { + enum qdl_storage_type storage_type; + char *source_name; + char *zip_name; + struct list_head node; +}; + +struct zipper_storage_ctx { + enum qdl_storage_type storage_type; + const char *memory; + xmlDoc *program_doc; + xmlNode *program_root; + xmlDoc *patch_doc; + xmlNode *patch_root; + char *program_xml_name; + char *patch_xml_name; + struct list_head node; +}; + +struct flashmap_json_buf { + char *buf; + size_t len; + size_t cap; + bool error; +}; + +static void zipper_free_name_entries(struct list_head *names) +{ + struct zipper_name_entry *entry; + struct zipper_name_entry *next; + + list_for_each_entry_safe(entry, next, names, node) { + list_del(&entry->node); + free(entry->name); + free(entry); + } +} + +static bool zipper_name_is_used(struct list_head *names, const char *name) +{ + struct zipper_name_entry *entry; + + list_for_each_entry(entry, names, node) { + if (!strcmp(entry->name, name)) + return true; + } + + return false; +} + +static int zipper_add_name_entry(struct list_head *names, const char *name) +{ + struct zipper_name_entry *entry; + + entry = calloc(1, sizeof(*entry)); + if (!entry) + return -1; + + entry->name = strdup(name); + if (!entry->name) { + free(entry); + return -1; + } + + list_append(names, &entry->node); + + return 0; +} + +static int zipper_add_buffer(zip_t *zip, const char *name, const void *data, size_t len) +{ + zip_source_t *source; + zip_int64_t idx; + void *copy = NULL; + + if (len) { + copy = malloc(len); + if (!copy) { + ux_err("failed to allocate zip member \"%s\"\n", name); + return -1; + } + memcpy(copy, data, len); + } + + source = zip_source_buffer(zip, copy, len, 1); + if (!source) { + ux_err("failed to create zip source for \"%s\"\n", name); + free(copy); + return -1; + } + + idx = zip_file_add(zip, name, source, ZIP_FL_OVERWRITE); + if (idx < 0) { + ux_err("failed to add \"%s\" to zip\n", name); + zip_source_free(source); + return -1; + } + + return 0; +} + +static int zipper_add_file(zip_t *zip, const char *name, const char *path) +{ + zip_source_t *source; + zip_int64_t idx; + + source = zip_source_file(zip, path, 0, -1); + if (!source) { + ux_err("failed to create zip file source for \"%s\"\n", path); + return -1; + } + + idx = zip_file_add(zip, name, source, ZIP_FL_OVERWRITE); + if (idx < 0) { + ux_err("failed to add \"%s\" to zip\n", name); + zip_source_free(source); + return -1; + } + + return 0; +} + +static void zipper_close_progress_cb(zip_t *archive __unused, + double progress, + void *userdata) +{ + unsigned int value; + const char *label = userdata ? (const char *)userdata : "finalize"; + + if (progress < 0.0) + progress = 0.0; + else if (progress > 1.0) + progress = 1.0; + + value = (unsigned int)(progress * 100000.0); + ux_progress(label, value, 100000); +} + +static struct zipper_file_rename *zipper_find_file_rename(struct list_head *renames, + enum qdl_storage_type storage_type, + const char *source_name) +{ + struct zipper_file_rename *rename; + + list_for_each_entry(rename, renames, node) { + if (rename->storage_type == storage_type && + !strcmp(rename->source_name, source_name)) + return rename; + } + + return NULL; +} + +static void zipper_free_file_renames(struct list_head *renames) +{ + struct zipper_file_rename *rename; + struct zipper_file_rename *next; + + list_for_each_entry_safe(rename, next, renames, node) { + list_del(&rename->node); + free(rename->source_name); + free(rename->zip_name); + free(rename); + } +} + +static struct zipper_storage_ctx *zipper_find_storage_ctx(struct list_head *storages, + enum qdl_storage_type storage_type) +{ + struct zipper_storage_ctx *storage; + + list_for_each_entry(storage, storages, node) { + if (storage->storage_type == storage_type) + return storage; + } + + return NULL; +} + +static void zipper_free_storage_contexts(struct list_head *storages) +{ + struct zipper_storage_ctx *storage; + struct zipper_storage_ctx *next; + + list_for_each_entry_safe(storage, next, storages, node) { + list_del(&storage->node); + xmlFreeDoc(storage->program_doc); + xmlFreeDoc(storage->patch_doc); + free(storage->program_xml_name); + free(storage->patch_xml_name); + free(storage); + } +} + +static struct zipper_storage_ctx *zipper_create_storage_ctx(enum qdl_storage_type storage_type) +{ + struct zipper_storage_ctx *storage; + const char *memory; + + memory = encode_storage_type(storage_type); + if (!memory) { + ux_err("unknown storage type %d\n", storage_type); + return NULL; + } + + storage = calloc(1, sizeof(*storage)); + if (!storage) + return NULL; + + storage->storage_type = storage_type; + storage->memory = memory; + + storage->program_doc = xmlNewDoc((xmlChar *)"1.0"); + storage->program_root = xmlNewNode(NULL, (xmlChar *)"data"); + if (!storage->program_doc || !storage->program_root) + goto err; + xmlDocSetRootElement(storage->program_doc, storage->program_root); + + storage->patch_doc = xmlNewDoc((xmlChar *)"1.0"); + storage->patch_root = xmlNewNode(NULL, (xmlChar *)"patches"); + if (!storage->patch_doc || !storage->patch_root) + goto err; + xmlDocSetRootElement(storage->patch_doc, storage->patch_root); + + return storage; + +err: + xmlFreeDoc(storage->program_doc); + xmlFreeDoc(storage->patch_doc); + free(storage); + return NULL; +} + +static int zipper_storage_append_program(struct zipper_storage_ctx *storage, struct firehose_op *op, + const char *filename, unsigned int file_offset) +{ + xmlNode *node; + + if (!filename) + return 0; + + node = xmlNewChild(storage->program_root, NULL, (xmlChar *)"program", NULL); + if (!node) + return -1; + + xml_setpropf(node, "SECTOR_SIZE_IN_BYTES", "%u", op->sector_size); + xml_setpropf(node, "filename", "%s", filename); + xml_setpropf(node, "num_partition_sectors", "%u", op->num_sectors); + xml_setpropf(node, "physical_partition_number", "%d", op->partition); + xml_setpropf(node, "start_sector", "%s", op->start_sector ? op->start_sector : "0"); + + if (op->label) + xml_setpropf(node, "label", "%s", op->label); + + if (op->is_nand) { + xml_setpropf(node, "PAGES_PER_BLOCK", "%u", op->pages_per_block); + xml_setpropf(node, "last_sector", "%u", op->last_sector); + } else { + xml_setpropf(node, "file_sector_offset", "%u", file_offset); + } + + return 0; +} + +static int zipper_register_raw_program_file(zip_t *zip, struct list_head *name_entries, + const char *memory, struct firehose_op *op, + unsigned int *raw_idx, char **zip_name) +{ + zip_source_t *source; + zip_int64_t idx; + uint64_t raw_size; + char *filename; + int ret; + + if (!op->sector_size || !op->num_sectors || op->sparse_offset < 0) { + ux_err("invalid sparse raw chunk parameters\n"); + return -1; + } + + if ((uint64_t)op->num_sectors > INT64_MAX / op->sector_size) { + ux_err("sparse raw chunk too large\n"); + return -1; + } + raw_size = (uint64_t)op->num_sectors * op->sector_size; + + filename = calloc(1, strlen(memory) + 31); + if (!filename) + return -1; + + snprintf(filename, strlen(memory) + 31, "%s_raw_%06u.bin", memory, (*raw_idx)++); + if (zipper_name_is_used(name_entries, filename)) { + ux_err("duplicate generated raw filename \"%s\"\n", filename); + goto err_free_filename; + } + + source = zip_source_file(zip, op->filename, (zip_uint64_t)op->sparse_offset, + (zip_int64_t)raw_size); + if (!source) { + ux_err("failed to create zip file source for \"%s\"\n", op->filename); + goto err_free_filename; + } + + idx = zip_file_add(zip, filename, source, ZIP_FL_OVERWRITE); + if (idx < 0) { + ux_err("failed to add \"%s\" to zip\n", filename); + goto err_free_zip_source; + } + + ret = zipper_add_name_entry(name_entries, filename); + if (ret) { + /* Don't free "source", it's owned by libzip since zip_file_add() */ + goto err_free_filename; + } + + *zip_name = filename; + return 0; + +err_free_zip_source: + zip_source_free(source); +err_free_filename: + free(filename); + return -1; +} + +static int zipper_register_fill_program_file(zip_t *zip, struct list_head *name_entries, + const char *memory, struct firehose_op *op, + unsigned int *fill_idx, char **zip_name) +{ + uint32_t fill_value = op->sparse_fill_value; + size_t fill_size; + char *filename; + void *buf; + size_t i; + int ret; + + if (!op->sector_size || !op->num_sectors) { + ux_err("invalid sparse fill chunk parameters\n"); + return -1; + } + + if ((size_t)op->num_sectors > SIZE_MAX / op->sector_size) { + ux_err("sparse fill chunk too large\n"); + return -1; + } + fill_size = (size_t)op->num_sectors * op->sector_size; + + filename = calloc(1, strlen(memory) + 32); + if (!filename) + return -1; + + snprintf(filename, strlen(memory) + 32, "%s_fill_%06u.bin", memory, (*fill_idx)++); + if (zipper_name_is_used(name_entries, filename)) { + ux_err("duplicate generated fill filename \"%s\"\n", filename); + free(filename); + return -1; + } + + buf = malloc(fill_size); + if (!buf) { + free(filename); + return -1; + } + + for (i = 0; i + sizeof(fill_value) <= fill_size; i += sizeof(fill_value)) + memcpy((char *)buf + i, &fill_value, sizeof(fill_value)); + while (i < fill_size) { + ((char *)buf)[i] = ((char *)&fill_value)[i % sizeof(fill_value)]; + i++; + } + + ret = zipper_add_buffer(zip, filename, buf, fill_size); + free(buf); + if (ret) { + free(filename); + return -1; + } + + ret = zipper_add_name_entry(name_entries, filename); + if (ret) { + free(filename); + return -1; + } + + *zip_name = filename; + return 0; +} + +static int zipper_register_program_file(zip_t *zip, struct list_head *name_entries, + struct list_head *renames, + enum qdl_storage_type storage_type, const char *memory, + const char *source_name, const char **zip_name) +{ + struct zipper_file_rename *rename; + char *file_basename; + char *renamed_file; + int ret; + + rename = zipper_find_file_rename(renames, storage_type, source_name); + if (rename) { + *zip_name = rename->zip_name; + return 0; + } + + file_basename = zipper_basename(source_name); + if (!file_basename) { + ux_err("failed to parse basename for \"%s\"\n", source_name); + return -1; + } + + if (asprintf(&renamed_file, "%s_%s", memory, file_basename) < 0) { + free(file_basename); + return -1; + } + free(file_basename); + + if (zipper_name_is_used(name_entries, renamed_file)) { + ux_err("duplicate zip filename \"%s\"\n", renamed_file); + free(renamed_file); + return -1; + } + + ret = zipper_add_file(zip, renamed_file, source_name); + if (ret) { + ux_err("failed to copy \"%s\"\n", source_name); + free(renamed_file); + return -1; + } + + ret = zipper_add_name_entry(name_entries, renamed_file); + if (ret) { + free(renamed_file); + return -1; + } + + rename = calloc(1, sizeof(*rename)); + if (!rename) { + free(renamed_file); + return -1; + } + + rename->storage_type = storage_type; + rename->source_name = strdup(source_name); + rename->zip_name = renamed_file; + if (!rename->source_name) { + free(rename->zip_name); + free(rename); + return -1; + } + + list_append(renames, &rename->node); + *zip_name = rename->zip_name; + + return 0; +} + +static int zipper_storage_append_erase(struct zipper_storage_ctx *storage, struct firehose_op *op) +{ + xmlNode *node; + + node = xmlNewChild(storage->program_root, NULL, (xmlChar *)"erase", NULL); + if (!node) + return -1; + + xml_setpropf(node, "SECTOR_SIZE_IN_BYTES", "%u", op->sector_size); + xml_setpropf(node, "num_partition_sectors", "%u", op->num_sectors); + xml_setpropf(node, "physical_partition_number", "%d", op->partition); + xml_setpropf(node, "start_sector", "%s", op->start_sector ? op->start_sector : "0"); + + if (op->is_nand) + xml_setpropf(node, "PAGES_PER_BLOCK", "%u", op->pages_per_block); + + return 0; +} + +static int zipper_storage_append_patch(struct zipper_storage_ctx *storage, struct firehose_op *op) +{ + xmlNode *node; + + node = xmlNewChild(storage->patch_root, NULL, (xmlChar *)"patch", NULL); + if (!node) + return -1; + + xml_setpropf(node, "SECTOR_SIZE_IN_BYTES", "%u", op->sector_size); + xml_setpropf(node, "byte_offset", "%u", op->byte_offset); + xml_setpropf(node, "filename", "%s", op->filename ? op->filename : ""); + xml_setpropf(node, "physical_partition_number", "%d", op->partition); + xml_setpropf(node, "size_in_bytes", "%u", op->size_in_bytes); + xml_setpropf(node, "start_sector", "%s", op->start_sector ? op->start_sector : "0"); + xml_setpropf(node, "value", "%s", op->value ? op->value : ""); + xml_setpropf(node, "what", "%s", op->what ? op->what : ""); + + return 0; +} + +static void flashmap_json_grow(struct flashmap_json_buf *json, size_t extra) +{ + size_t needed; + size_t next_cap; + char *new_buf; + + if (json->error) + return; + + needed = json->len + extra + 1; + if (needed <= json->cap) + return; + + next_cap = json->cap ? json->cap : 256; + while (next_cap < needed) { + if (next_cap > SIZE_MAX / 2) { + json->error = true; + return; + } + next_cap *= 2; + } + + new_buf = realloc(json->buf, next_cap); + if (!new_buf) { + json->error = true; + return; + } + + json->buf = new_buf; + json->cap = next_cap; +} + +static void flashmap_json_append_raw(struct flashmap_json_buf *json, const char *s, size_t len) +{ + if (json->error) + return; + + flashmap_json_grow(json, len); + if (json->error) + return; + + memcpy(json->buf + json->len, s, len); + json->len += len; + json->buf[json->len] = '\0'; +} + +static void flashmap_json_appendf(struct flashmap_json_buf *json, const char *fmt, ...) +{ + va_list ap; + va_list aq; + int needed; + + if (json->error) + return; + + va_start(ap, fmt); + va_copy(aq, ap); + needed = vsnprintf(NULL, 0, fmt, aq); + va_end(aq); + if (needed < 0) { + va_end(ap); + json->error = true; + return; + } + + flashmap_json_grow(json, needed); + if (json->error) { + va_end(ap); + return; + } + + vsnprintf(json->buf + json->len, json->cap - json->len, fmt, ap); + json->len += needed; + va_end(ap); +} + +static void flashmap_json_append_escaped_string(struct flashmap_json_buf *json, const char *s) +{ + unsigned char ch; + char esc[7]; + + flashmap_json_append_raw(json, "\"", 1); + + for (; !json->error && *s; s++) { + ch = (unsigned char)*s; + switch (ch) { + case '"': + flashmap_json_append_raw(json, "\\\"", 2); + break; + case '\\': + flashmap_json_append_raw(json, "\\\\", 2); + break; + case '\b': + flashmap_json_append_raw(json, "\\b", 2); + break; + case '\f': + flashmap_json_append_raw(json, "\\f", 2); + break; + case '\n': + flashmap_json_append_raw(json, "\\n", 2); + break; + case '\r': + flashmap_json_append_raw(json, "\\r", 2); + break; + case '\t': + flashmap_json_append_raw(json, "\\t", 2); + break; + default: + if (ch < 0x20) { + snprintf(esc, sizeof(esc), "\\u%04x", ch); + flashmap_json_append_raw(json, esc, 6); + } else { + flashmap_json_append_raw(json, (const char *)&ch, 1); + } + break; + } + } + + flashmap_json_append_raw(json, "\"", 1); +} + +static int zipper_add_programmer_images(zip_t *zip, struct list_head *name_entries, + char **programmer_entries, + struct sahara_image *images) +{ + char *basename_name; + char *zip_name; + int count = 0; + int i; + int ret; + + for (i = 0; i < MAPPING_SZ; i++) { + if (!images[i].ptr) + continue; + + basename_name = zipper_basename(images[i].name ? images[i].name : "programmer.bin"); + if (!basename_name) + return -1; + + if (zipper_name_is_used(name_entries, basename_name)) { + zip_name = calloc(1, strlen(basename_name) + 20); + if (!zip_name) { + free(basename_name); + return -1; + } + snprintf(zip_name, strlen(basename_name) + 20, "sahara%u_%s", i, basename_name); + free(basename_name); + } else { + zip_name = basename_name; + } + + if (zipper_name_is_used(name_entries, zip_name)) { + ux_err("duplicate zip filename \"%s\"\n", zip_name); + free(zip_name); + return -1; + } + + ret = zipper_add_buffer(zip, zip_name, images[i].ptr, images[i].len); + if (ret) { + free(zip_name); + return ret; + } + + ret = zipper_add_name_entry(name_entries, zip_name); + if (ret) { + free(zip_name); + return ret; + } + + programmer_entries[i] = zip_name; + count++; + } + + return count ? 0 : -1; +} + +static void zipper_free_programmer_entries(char **programmer_entries) +{ + int i; + + for (i = 0; i < MAPPING_SZ; i++) { + free(programmer_entries[i]); + programmer_entries[i] = NULL; + } +} + +static int zipper_add_storage_xml(zip_t *zip, struct list_head *name_entries, + struct zipper_storage_ctx *storage) +{ + xmlChar *xml_data; + int len; + int ret; + + storage->program_xml_name = calloc(1, strlen(storage->memory) + strlen("_rawprogram.xml") + 1); + if (!storage->program_xml_name) + return -1; + snprintf(storage->program_xml_name, strlen(storage->memory) + strlen("_rawprogram.xml") + 1, + "%s_rawprogram.xml", storage->memory); + + storage->patch_xml_name = calloc(1, strlen(storage->memory) + strlen("_patch.xml") + 1); + if (!storage->patch_xml_name) + return -1; + snprintf(storage->patch_xml_name, strlen(storage->memory) + strlen("_patch.xml") + 1, + "%s_patch.xml", storage->memory); + + if (zipper_name_is_used(name_entries, storage->program_xml_name) || + zipper_name_is_used(name_entries, storage->patch_xml_name)) { + ux_err("duplicate xml filename for memory \"%s\"\n", storage->memory); + return -1; + } + + xmlDocDumpMemory(storage->program_doc, &xml_data, &len); + if (!xml_data || len < 0) + return -1; + ret = zipper_add_buffer(zip, storage->program_xml_name, xml_data, (size_t)len); + xmlFree(xml_data); + if (ret) + return ret; + ret = zipper_add_name_entry(name_entries, storage->program_xml_name); + if (ret) + return ret; + + xmlDocDumpMemory(storage->patch_doc, &xml_data, &len); + if (!xml_data || len < 0) + return -1; + ret = zipper_add_buffer(zip, storage->patch_xml_name, xml_data, (size_t)len); + xmlFree(xml_data); + if (ret) + return ret; + ret = zipper_add_name_entry(name_entries, storage->patch_xml_name); + if (ret) + return ret; + + return 0; +} + +static int flashmap_build_json(struct flashmap_json_buf *json, + char **programmer_entries, + struct list_head *storages) +{ + struct zipper_storage_ctx *storage; + bool first; + int i; + + flashmap_json_appendf(json, "{\n"); + flashmap_json_appendf(json, " \"version\": \"1.2.0-qdl\",\n"); + flashmap_json_appendf(json, " \"products\": [\n"); + flashmap_json_appendf(json, " {\n"); + flashmap_json_appendf(json, " \"name\": \"contents\",\n"); + flashmap_json_appendf(json, " \"layouts\": [\n"); + flashmap_json_appendf(json, " {\n"); + flashmap_json_appendf(json, " \"name\": \"layout0\",\n"); + flashmap_json_appendf(json, " \"programmer\": {\n"); + + first = true; + for (i = 0; i < MAPPING_SZ; i++) { + if (!programmer_entries[i]) + continue; + + if (!first) + flashmap_json_appendf(json, ",\n"); + first = false; + + flashmap_json_appendf(json, " \"%u\": ", i); + flashmap_json_append_escaped_string(json, programmer_entries[i]); + } + flashmap_json_appendf(json, "\n"); + flashmap_json_appendf(json, " },\n"); + flashmap_json_appendf(json, " \"programmable\": [\n"); + + first = true; + list_for_each_entry(storage, storages, node) { + if (!first) + flashmap_json_appendf(json, ",\n"); + first = false; + + flashmap_json_appendf(json, " {\n"); + flashmap_json_appendf(json, " \"memory\": \"%s\",\n", storage->memory); + flashmap_json_appendf(json, " \"slot\": 0,\n"); + flashmap_json_appendf(json, " \"files\": [\n"); + flashmap_json_appendf(json, " "); + flashmap_json_append_escaped_string(json, storage->program_xml_name); + flashmap_json_appendf(json, ",\n"); + flashmap_json_appendf(json, " "); + flashmap_json_append_escaped_string(json, storage->patch_xml_name); + flashmap_json_appendf(json, "\n"); + flashmap_json_appendf(json, " ]\n"); + flashmap_json_appendf(json, " }"); + } + + flashmap_json_appendf(json, "\n"); + flashmap_json_appendf(json, " ]\n"); + flashmap_json_appendf(json, " }\n"); + flashmap_json_appendf(json, " ]\n"); + flashmap_json_appendf(json, " }\n"); + flashmap_json_appendf(json, " ]\n"); + flashmap_json_appendf(json, "}\n"); + + return json->error ? -1 : 0; +} + +int zipper_write(const char *filename, struct list_head *ops, struct sahara_image *images) +{ + char *programmer_entries[MAPPING_SZ] = {0}; + struct list_head file_renames = LIST_INIT(file_renames); + struct zipper_storage_ctx *storage = NULL; + struct list_head storages = LIST_INIT(storages); + struct list_head name_entries = LIST_INIT(name_entries); + struct flashmap_json_buf json = {0}; + enum qdl_storage_type current_storage = QDL_STORAGE_UNKNOWN; + unsigned int raw_idx = 0; + unsigned int fill_idx = 0; + struct firehose_op *op; + unsigned int file_offset; + const char *zip_name; + char *sparse_zip_name; + char *tmp_filename = NULL; + zip_t *zip; + int tmp_fd; + unsigned int programmer_count = 0; + unsigned int program_count = 0; + unsigned int patch_count = 0; + unsigned int erase_count = 0; + int zip_error = 0; + int ret = -1; + + tmp_filename = malloc(strlen(filename) + 8); + if (!tmp_filename) + goto out_cleanup; + sprintf(tmp_filename, "%s.XXXXXX", filename); + + tmp_fd = mkstemp(tmp_filename); + if (tmp_fd < 0) { + ux_err("failed to create temporary output zip \"%s\": %s\n", + tmp_filename, strerror(errno)); + goto out_cleanup; + } + close(tmp_fd); + + zip = zip_open(tmp_filename, ZIP_CREATE | ZIP_TRUNCATE, &zip_error); + if (!zip) { + ux_err("failed to open output zip \"%s\"\n", tmp_filename); + ret = -1; + goto out_cleanup; + } + + list_for_each_entry(op, ops, node) { + if (op->type == FIREHOSE_OP_CONFIGURE) { + current_storage = op->storage_type; + storage = zipper_find_storage_ctx(&storages, current_storage); + if (!storage) { + storage = zipper_create_storage_ctx(current_storage); + if (!storage) + goto out_discard_zip; + + list_append(&storages, &storage->node); + } + continue; + } + + if (!storage) { + ux_err("internal error: missing configure operation\n"); + goto out_discard_zip; + } + + switch (op->type) { + case FIREHOSE_OP_PROGRAM: + if (!op->filename) + break; + if (op->zip) { + ux_err("create-zip does not support program entries sourced from zip archives\n"); + goto out_discard_zip; + } + + sparse_zip_name = NULL; + zip_name = NULL; + + if (op->sparse) { + switch (op->sparse_chunk_type) { + case CHUNK_TYPE_RAW: + ret = zipper_register_raw_program_file(zip, &name_entries, storage->memory, op, + &raw_idx, &sparse_zip_name); + break; + case CHUNK_TYPE_FILL: + ret = zipper_register_fill_program_file(zip, &name_entries, storage->memory, op, + &fill_idx, &sparse_zip_name); + break; + default: + ux_err("unsupported sparse chunk type %u in \"%s\"\n", + op->sparse_chunk_type, op->filename); + goto out_discard_zip; + } + if (ret) + goto out_discard_zip; + + zip_name = sparse_zip_name; + file_offset = 0; + } else { + ret = zipper_register_program_file(zip, &name_entries, &file_renames, + current_storage, storage->memory, + op->filename, &zip_name); + if (ret) + goto out_discard_zip; + + file_offset = op->file_offset; + } + + ret = zipper_storage_append_program(storage, op, zip_name, file_offset); + free(sparse_zip_name); + if (ret) + goto out_discard_zip; + program_count++; + break; + case FIREHOSE_OP_ERASE: + ret = zipper_storage_append_erase(storage, op); + if (ret) + goto out_discard_zip; + erase_count++; + break; + case FIREHOSE_OP_PATCH: + /* + * Only filename=="DISK" entries are actually flashed, + * so omit the others from the zip file as well. + */ + if (!op->filename || strcmp(op->filename, "DISK")) + break; + + ret = zipper_storage_append_patch(storage, op); + if (ret) + goto out_discard_zip; + patch_count++; + break; + default: + break; + } + } + + if (list_empty(&storages)) { + ux_err("no configurable storage operations to write\n"); + goto out_discard_zip; + } + + ret = zipper_add_programmer_images(zip, &name_entries, programmer_entries, images); + if (ret) { + ux_err("no programmer images available to write\n"); + goto out_discard_zip; + } + for (int i = 0; i < MAPPING_SZ; i++) { + if (programmer_entries[i]) + programmer_count++; + } + + list_for_each_entry(storage, &storages, node) { + ret = zipper_add_storage_xml(zip, &name_entries, storage); + if (ret) + goto out_discard_zip; + } + + ret = flashmap_build_json(&json, programmer_entries, &storages); + if (ret) + goto out_discard_zip; + + ret = zipper_add_buffer(zip, "flashmap.json", json.buf, json.len); + if (ret) + goto out_discard_zip; + + ux_info("found %u programmers, %u program entries, %u patch entries, %u erase entries\n", + programmer_count, program_count, patch_count, erase_count); + ux_info("finalizing archive, this can take a while for large images\n"); + if (zip_register_progress_callback_with_state(zip, 0.001, + zipper_close_progress_cb, + NULL, (void *)filename) < 0) + ux_debug("unable to register zip close progress callback\n"); + + if (zip_close(zip) < 0) { + ux_err("failed to finalize output zip\n"); + zip_discard(zip); + ret = -1; + goto out_cleanup; + } + + if (rename(tmp_filename, filename) < 0) { + ux_err("failed to rename temporary output zip \"%s\" to \"%s\": %s\n", + tmp_filename, filename, strerror(errno)); + ret = -1; + goto out_cleanup; + } + + ux_info("successfully created %s\n", filename); + + ret = 0; + goto out_cleanup; + +out_discard_zip: + zip_discard(zip); + +out_cleanup: + if (ret && tmp_filename) + unlink(tmp_filename); + free(tmp_filename); + free(json.buf); + zipper_free_name_entries(&name_entries); + zipper_free_file_renames(&file_renames); + zipper_free_programmer_entries(programmer_entries); + zipper_free_storage_contexts(&storages); + + return ret; +}