2026-04-23 19:57:05 +00:00
|
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
|
#define _FILE_OFFSET_BITS 64
|
|
|
|
|
#if defined(__APPLE__)
|
|
|
|
|
#define _DARWIN_C_SOURCE
|
|
|
|
|
#endif
|
|
|
|
|
#define _XOPEN_SOURCE 700
|
|
|
|
|
|
|
|
|
|
#include <errno.h>
|
|
|
|
|
#include <limits.h>
|
|
|
|
|
#include <setjmp.h>
|
|
|
|
|
#include <stdarg.h>
|
|
|
|
|
#include <stdbool.h>
|
|
|
|
|
#include <stddef.h>
|
|
|
|
|
#include <stdint.h>
|
|
|
|
|
#include <stdio.h>
|
|
|
|
|
#include <stdlib.h>
|
|
|
|
|
#include <string.h>
|
|
|
|
|
#include <sys/types.h>
|
|
|
|
|
#include <unistd.h>
|
|
|
|
|
|
|
|
|
|
#include <cmocka.h>
|
|
|
|
|
#include <libxml/parser.h>
|
|
|
|
|
#include <libxml/tree.h>
|
|
|
|
|
|
|
|
|
|
#include "file.h"
|
|
|
|
|
#include "firehose.h"
|
|
|
|
|
#include "list.h"
|
2026-04-30 22:24:52 -05:00
|
|
|
#include "pathbuf.h"
|
2026-04-23 19:57:05 +00:00
|
|
|
#include "program.h"
|
|
|
|
|
#include "qdl.h"
|
|
|
|
|
#include "sparse.h"
|
2026-04-30 22:27:31 -05:00
|
|
|
#include "common.h"
|
2026-04-23 19:57:05 +00:00
|
|
|
|
|
|
|
|
#define TEST_INCDIR "/mock/incdir"
|
|
|
|
|
#define TEST_XMLDIR "/mock/xml"
|
|
|
|
|
#define TEST_PROGRAM_FILE TEST_XMLDIR "/program.xml"
|
|
|
|
|
#define TEST_PAYLOAD "payload.bin"
|
|
|
|
|
#define TEST_INCDIR_PAYLOAD TEST_INCDIR "/" TEST_PAYLOAD
|
|
|
|
|
#define TEST_XMLDIR_PAYLOAD TEST_XMLDIR "/" TEST_PAYLOAD
|
|
|
|
|
|
|
|
|
|
bool qdl_debug;
|
|
|
|
|
|
|
|
|
|
static const char *existing_paths[4];
|
|
|
|
|
|
|
|
|
|
static void set_existing_paths(const char *path0, const char *path1, const char *path2)
|
|
|
|
|
{
|
|
|
|
|
existing_paths[0] = path0;
|
|
|
|
|
existing_paths[1] = path1;
|
|
|
|
|
existing_paths[2] = path2;
|
|
|
|
|
existing_paths[3] = NULL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static bool mock_path_exists(const char *path)
|
|
|
|
|
{
|
|
|
|
|
for (size_t i = 0; existing_paths[i]; i++) {
|
|
|
|
|
if (!strcmp(path, existing_paths[i]))
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int mock_access(const char *path, int mode)
|
|
|
|
|
{
|
|
|
|
|
assert_int_equal(mode, F_OK);
|
|
|
|
|
|
|
|
|
|
if (mock_path_exists(path))
|
|
|
|
|
return 0;
|
|
|
|
|
|
|
|
|
|
errno = ENOENT;
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int qdl_file_open(struct qdl_zip *qzip, const char *filename, struct qdl_file *file)
|
|
|
|
|
{
|
|
|
|
|
assert_null(qzip);
|
|
|
|
|
|
|
|
|
|
if (!mock_path_exists(filename))
|
|
|
|
|
return -1;
|
|
|
|
|
|
|
|
|
|
file->type = QDL_FILE_TYPE_POSIX;
|
|
|
|
|
file->size = 1;
|
|
|
|
|
file->fd = -1;
|
|
|
|
|
file->zip_file = NULL;
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void qdl_file_close(struct qdl_file *file)
|
|
|
|
|
{
|
|
|
|
|
file->type = QDL_FILE_TYPE_UNKNOWN;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void *qdl_file_load(struct qdl_file *file, size_t *len)
|
|
|
|
|
{
|
|
|
|
|
(void)file;
|
|
|
|
|
(void)len;
|
|
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
size_t qdl_file_getsize(struct qdl_file *file)
|
|
|
|
|
{
|
|
|
|
|
return file->size;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
off_t qdl_file_seek(struct qdl_file *file, off_t offset, int whence)
|
|
|
|
|
{
|
|
|
|
|
(void)file;
|
|
|
|
|
(void)offset;
|
|
|
|
|
(void)whence;
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ssize_t qdl_file_read(struct qdl_file *file, void *buf, size_t len)
|
|
|
|
|
{
|
|
|
|
|
(void)file;
|
|
|
|
|
(void)buf;
|
|
|
|
|
(void)len;
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct qdl_zip *qdl_zip_get(struct qdl_zip *qzip)
|
|
|
|
|
{
|
|
|
|
|
return qzip;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void qdl_zip_put(struct qdl_zip *qzip)
|
|
|
|
|
{
|
|
|
|
|
(void)qzip;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int sparse_header_parse(struct qdl_file *file, sparse_header_t *sparse_header)
|
|
|
|
|
{
|
|
|
|
|
(void)file;
|
|
|
|
|
(void)sparse_header;
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int sparse_chunk_header_parse(struct qdl_file *file,
|
|
|
|
|
sparse_header_t *sparse_header,
|
|
|
|
|
uint64_t *chunk_size,
|
|
|
|
|
uint32_t *value, off_t *offset)
|
|
|
|
|
{
|
|
|
|
|
(void)file;
|
|
|
|
|
(void)sparse_header;
|
|
|
|
|
(void)chunk_size;
|
|
|
|
|
(void)value;
|
|
|
|
|
(void)offset;
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ux_init(void)
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ux_err(const char *fmt, ...)
|
|
|
|
|
{
|
|
|
|
|
(void)fmt;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ux_info(const char *fmt, ...)
|
|
|
|
|
{
|
|
|
|
|
(void)fmt;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ux_log(const char *fmt, ...)
|
|
|
|
|
{
|
|
|
|
|
(void)fmt;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ux_debug(const char *fmt, ...)
|
|
|
|
|
{
|
|
|
|
|
(void)fmt;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ux_progress(const char *fmt, unsigned int value, unsigned int size, ...)
|
|
|
|
|
{
|
|
|
|
|
(void)fmt;
|
|
|
|
|
(void)value;
|
|
|
|
|
(void)size;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct firehose_op *firehose_alloc_op(int type)
|
|
|
|
|
{
|
|
|
|
|
struct firehose_op *op;
|
|
|
|
|
|
|
|
|
|
op = calloc(1, sizeof(*op));
|
|
|
|
|
if (!op)
|
|
|
|
|
return NULL;
|
|
|
|
|
|
|
|
|
|
op->type = type;
|
|
|
|
|
return op;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void firehose_free_ops(struct list_head *ops)
|
|
|
|
|
{
|
|
|
|
|
struct firehose_op *next;
|
|
|
|
|
struct firehose_op *op;
|
|
|
|
|
|
|
|
|
|
list_for_each_entry_safe(op, next, ops, node) {
|
|
|
|
|
list_del(&op->node);
|
|
|
|
|
qdl_zip_put(op->zip);
|
|
|
|
|
free((void *)op->filename);
|
|
|
|
|
free((void *)op->label);
|
|
|
|
|
free((void *)op->start_sector);
|
|
|
|
|
free((void *)op->gpt_partition);
|
|
|
|
|
free((void *)op->value);
|
|
|
|
|
free((void *)op->what);
|
|
|
|
|
free(op);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-30 22:24:52 -05:00
|
|
|
int contents_resolve_path(struct contents_filter *filter, const char *filename, struct pathbuf *path)
|
|
|
|
|
{
|
|
|
|
|
(void)filter;
|
|
|
|
|
(void)filename;
|
|
|
|
|
(void)path;
|
|
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-23 19:57:05 +00:00
|
|
|
static xmlDoc *build_program_doc(const char *filename)
|
|
|
|
|
{
|
|
|
|
|
char xml[1024];
|
|
|
|
|
int ret;
|
|
|
|
|
|
|
|
|
|
ret = snprintf(xml, sizeof(xml),
|
|
|
|
|
"<data>"
|
|
|
|
|
"<program "
|
|
|
|
|
"SECTOR_SIZE_IN_BYTES=\"512\" "
|
|
|
|
|
"filename=\"%s\" "
|
|
|
|
|
"label=\"test\" "
|
|
|
|
|
"num_partition_sectors=\"1\" "
|
|
|
|
|
"physical_partition_number=\"0\" "
|
|
|
|
|
"start_sector=\"0\" "
|
|
|
|
|
"file_sector_offset=\"0\""
|
|
|
|
|
"/>"
|
|
|
|
|
"</data>",
|
|
|
|
|
filename);
|
|
|
|
|
assert_true(ret > 0 && (size_t)ret < sizeof(xml));
|
|
|
|
|
|
|
|
|
|
return xmlReadMemory(xml, ret, "program.xml", NULL, 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static struct firehose_op *get_only_op(struct list_head *ops)
|
|
|
|
|
{
|
|
|
|
|
struct firehose_op *op;
|
|
|
|
|
struct list_head *it;
|
|
|
|
|
size_t count = 0;
|
|
|
|
|
|
|
|
|
|
list_for_each(it, ops)
|
|
|
|
|
count++;
|
|
|
|
|
|
|
|
|
|
if (count != 1)
|
|
|
|
|
fail_msg("expected exactly one firehose op, got %zu", count);
|
|
|
|
|
|
|
|
|
|
op = list_entry_first(ops, struct firehose_op, node);
|
|
|
|
|
if (op->type != FIREHOSE_OP_PROGRAM)
|
|
|
|
|
fail_msg("expected firehose op type PROGRAM, got %d", op->type);
|
|
|
|
|
|
|
|
|
|
return op;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void test_incdir_takes_precedence(void **state)
|
|
|
|
|
{
|
|
|
|
|
struct firehose_op *op;
|
|
|
|
|
struct list_head ops = LIST_INIT(ops);
|
|
|
|
|
xmlDoc *doc;
|
|
|
|
|
int ret;
|
|
|
|
|
|
|
|
|
|
(void)state;
|
|
|
|
|
set_existing_paths(TEST_INCDIR_PAYLOAD, TEST_XMLDIR_PAYLOAD, NULL);
|
|
|
|
|
|
|
|
|
|
doc = build_program_doc(TEST_PAYLOAD);
|
|
|
|
|
if (!doc)
|
|
|
|
|
fail_msg("failed to parse synthetic program XML");
|
|
|
|
|
|
2026-04-30 22:24:52 -05:00
|
|
|
ret = program_load_xml(&ops, doc, NULL, TEST_PROGRAM_FILE, false, false, NULL, TEST_INCDIR);
|
2026-04-23 19:57:05 +00:00
|
|
|
if (ret)
|
|
|
|
|
fail_msg("incdir payload should be accepted, got %d", ret);
|
|
|
|
|
|
|
|
|
|
op = get_only_op(&ops);
|
|
|
|
|
if (!op->filename || strcmp(op->filename, TEST_INCDIR_PAYLOAD))
|
|
|
|
|
fail_msg("incdir should take precedence, got '%s'",
|
|
|
|
|
op->filename ? op->filename : "(null)");
|
|
|
|
|
|
|
|
|
|
firehose_free_ops(&ops);
|
|
|
|
|
xmlFreeDoc(doc);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void test_program_xml_directory_is_used(void **state)
|
|
|
|
|
{
|
|
|
|
|
struct firehose_op *op;
|
|
|
|
|
struct list_head ops = LIST_INIT(ops);
|
|
|
|
|
xmlDoc *doc;
|
|
|
|
|
int ret;
|
|
|
|
|
|
|
|
|
|
(void)state;
|
|
|
|
|
set_existing_paths(TEST_XMLDIR_PAYLOAD, NULL, NULL);
|
|
|
|
|
|
|
|
|
|
doc = build_program_doc(TEST_PAYLOAD);
|
|
|
|
|
if (!doc)
|
|
|
|
|
fail_msg("failed to parse synthetic program XML");
|
|
|
|
|
|
2026-04-30 22:24:52 -05:00
|
|
|
ret = program_load_xml(&ops, doc, NULL, TEST_PROGRAM_FILE, false, false, NULL, TEST_INCDIR);
|
2026-04-23 19:57:05 +00:00
|
|
|
if (ret)
|
|
|
|
|
fail_msg("XML-adjacent payload should be accepted, got %d", ret);
|
|
|
|
|
|
|
|
|
|
op = get_only_op(&ops);
|
|
|
|
|
if (!op->filename || strcmp(op->filename, TEST_XMLDIR_PAYLOAD))
|
|
|
|
|
fail_msg("program XML directory should be used, got '%s'",
|
|
|
|
|
op->filename ? op->filename : "(null)");
|
|
|
|
|
|
|
|
|
|
firehose_free_ops(&ops);
|
|
|
|
|
xmlFreeDoc(doc);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void test_program_xml_directory_takes_precedence_over_current_directory(void **state)
|
|
|
|
|
{
|
|
|
|
|
struct firehose_op *op;
|
|
|
|
|
struct list_head ops = LIST_INIT(ops);
|
|
|
|
|
xmlDoc *doc;
|
|
|
|
|
int ret;
|
|
|
|
|
|
|
|
|
|
(void)state;
|
|
|
|
|
set_existing_paths(TEST_XMLDIR_PAYLOAD, TEST_PAYLOAD, NULL);
|
|
|
|
|
|
|
|
|
|
doc = build_program_doc(TEST_PAYLOAD);
|
|
|
|
|
if (!doc)
|
|
|
|
|
fail_msg("failed to parse synthetic program XML");
|
|
|
|
|
|
2026-04-30 22:24:52 -05:00
|
|
|
ret = program_load_xml(&ops, doc, NULL, TEST_PROGRAM_FILE, false, false, NULL, TEST_INCDIR);
|
2026-04-23 19:57:05 +00:00
|
|
|
if (ret)
|
|
|
|
|
fail_msg("XML-adjacent payload should beat current-directory fallback, got %d", ret);
|
|
|
|
|
|
|
|
|
|
op = get_only_op(&ops);
|
|
|
|
|
if (!op->filename || strcmp(op->filename, TEST_XMLDIR_PAYLOAD))
|
|
|
|
|
fail_msg("program XML directory should take precedence over current directory, got '%s'",
|
|
|
|
|
op->filename ? op->filename : "(null)");
|
|
|
|
|
|
|
|
|
|
firehose_free_ops(&ops);
|
|
|
|
|
xmlFreeDoc(doc);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void test_current_directory_is_used_when_path_resolution_misses(void **state)
|
|
|
|
|
{
|
|
|
|
|
struct firehose_op *op;
|
|
|
|
|
struct list_head ops = LIST_INIT(ops);
|
|
|
|
|
xmlDoc *doc;
|
|
|
|
|
int ret;
|
|
|
|
|
|
|
|
|
|
(void)state;
|
|
|
|
|
set_existing_paths(TEST_PAYLOAD, NULL, NULL);
|
|
|
|
|
|
|
|
|
|
doc = build_program_doc(TEST_PAYLOAD);
|
|
|
|
|
if (!doc)
|
|
|
|
|
fail_msg("failed to parse synthetic program XML");
|
|
|
|
|
|
|
|
|
|
/* Compatibility fallback: leave the original relative filename intact. */
|
2026-04-30 22:24:52 -05:00
|
|
|
ret = program_load_xml(&ops, doc, NULL, TEST_PROGRAM_FILE, false, false, NULL, TEST_INCDIR);
|
2026-04-23 19:57:05 +00:00
|
|
|
if (ret)
|
|
|
|
|
fail_msg("relative fallback payload should be accepted, got %d", ret);
|
|
|
|
|
|
|
|
|
|
op = get_only_op(&ops);
|
|
|
|
|
if (!op->filename || strcmp(op->filename, TEST_PAYLOAD))
|
|
|
|
|
fail_msg("unresolved relative filename should be preserved, got '%s'",
|
|
|
|
|
op->filename ? op->filename : "(null)");
|
|
|
|
|
|
|
|
|
|
firehose_free_ops(&ops);
|
|
|
|
|
xmlFreeDoc(doc);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void test_missing_file_fails_without_allow_missing(void **state)
|
|
|
|
|
{
|
|
|
|
|
struct list_head ops = LIST_INIT(ops);
|
|
|
|
|
xmlDoc *doc;
|
|
|
|
|
int ret;
|
|
|
|
|
|
|
|
|
|
(void)state;
|
|
|
|
|
set_existing_paths(NULL, NULL, NULL);
|
|
|
|
|
|
|
|
|
|
doc = build_program_doc(TEST_PAYLOAD);
|
|
|
|
|
if (!doc)
|
|
|
|
|
fail_msg("failed to parse synthetic program XML");
|
|
|
|
|
|
2026-04-30 22:24:52 -05:00
|
|
|
ret = program_load_xml(&ops, doc, NULL, TEST_PROGRAM_FILE, false, false, NULL, TEST_INCDIR);
|
2026-04-23 19:57:05 +00:00
|
|
|
if (ret != -1)
|
|
|
|
|
fail_msg("missing payload should fail without allow_missing, got %d", ret);
|
|
|
|
|
if (!list_empty(&ops))
|
|
|
|
|
fail_msg("missing payload without allow_missing should not append firehose ops");
|
|
|
|
|
|
|
|
|
|
xmlFreeDoc(doc);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void test_missing_file_is_tolerated_with_allow_missing(void **state)
|
|
|
|
|
{
|
|
|
|
|
struct firehose_op *op;
|
|
|
|
|
struct list_head ops = LIST_INIT(ops);
|
|
|
|
|
xmlDoc *doc;
|
|
|
|
|
int ret;
|
|
|
|
|
|
|
|
|
|
(void)state;
|
|
|
|
|
set_existing_paths(NULL, NULL, NULL);
|
|
|
|
|
|
|
|
|
|
doc = build_program_doc(TEST_PAYLOAD);
|
|
|
|
|
if (!doc)
|
|
|
|
|
fail_msg("failed to parse synthetic program XML");
|
|
|
|
|
|
2026-04-30 22:24:52 -05:00
|
|
|
ret = program_load_xml(&ops, doc, NULL, TEST_PROGRAM_FILE, false, true, NULL, TEST_INCDIR);
|
2026-04-23 19:57:05 +00:00
|
|
|
if (ret)
|
|
|
|
|
fail_msg("missing payload should be tolerated with allow_missing, got %d", ret);
|
|
|
|
|
|
|
|
|
|
op = get_only_op(&ops);
|
|
|
|
|
if (op->filename)
|
|
|
|
|
fail_msg("allow_missing should clear the missing program filename, got '%s'",
|
|
|
|
|
op->filename);
|
|
|
|
|
|
|
|
|
|
firehose_free_ops(&ops);
|
|
|
|
|
xmlFreeDoc(doc);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int main(void)
|
|
|
|
|
{
|
|
|
|
|
const struct CMUnitTest tests[] = {
|
|
|
|
|
cmocka_unit_test(test_incdir_takes_precedence),
|
|
|
|
|
cmocka_unit_test(test_program_xml_directory_is_used),
|
|
|
|
|
cmocka_unit_test(test_program_xml_directory_takes_precedence_over_current_directory),
|
|
|
|
|
cmocka_unit_test(test_current_directory_is_used_when_path_resolution_misses),
|
|
|
|
|
cmocka_unit_test(test_missing_file_fails_without_allow_missing),
|
|
|
|
|
cmocka_unit_test(test_missing_file_is_tolerated_with_allow_missing),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return cmocka_run_group_tests(tests, NULL, NULL);
|
|
|
|
|
}
|