diff --git a/components/esp_blockdev_util/generic_partition.c b/components/esp_blockdev_util/generic_partition.c new file mode 100644 index 0000000000..92560f063f --- /dev/null +++ b/components/esp_blockdev_util/generic_partition.c @@ -0,0 +1,226 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include + +#include "esp_blockdev.h" +#include "esp_err.h" +#include "esp_check.h" + +#include "esp_blockdev/generic_partition.h" + +static const char *TAG = "esp_blockdev/generic_partition"; + +typedef struct { + esp_blockdev_t dev; + esp_blockdev_handle_t parent; + size_t start_offset; +} esp_blockdev_generic_partition_t; + +ssize_t esp_blockdev_generic_partition_translate_address_to_parent(esp_blockdev_handle_t dev_handle, size_t address) +{ + ESP_RETURN_ON_FALSE(dev_handle != NULL, -1, TAG, "The dev_handle cannot be NULL"); + + esp_blockdev_generic_partition_t *dev = (esp_blockdev_generic_partition_t *)dev_handle; + esp_blockdev_handle_t parent = dev->parent; + + ESP_RETURN_ON_FALSE(address <= dev->dev.geometry.disk_size, -1, TAG, "The address falls outside of the partition"); + + uint64_t translated = (uint64_t)dev->start_offset + (uint64_t)address; + ESP_RETURN_ON_FALSE(translated >= (uint64_t)address, -1, TAG, "Address translation overflowed"); + ESP_RETURN_ON_FALSE(translated <= parent->geometry.disk_size, -1, TAG, "The address range falls outside of the parent device"); + + return (ssize_t)translated; +} + +ssize_t esp_blockdev_generic_partition_translate_address_to_child(esp_blockdev_handle_t dev_handle, size_t address) +{ + ESP_RETURN_ON_FALSE(dev_handle != NULL, -1, TAG, "The dev_handle cannot be NULL"); + + esp_blockdev_generic_partition_t *dev = (esp_blockdev_generic_partition_t *)dev_handle; + + uint64_t start_offset = dev->start_offset; + ESP_RETURN_ON_FALSE((uint64_t)address >= start_offset, -1, TAG, "The parent address is below the partition base"); + + uint64_t translated = (uint64_t)address - start_offset; + ESP_RETURN_ON_FALSE(translated <= dev->dev.geometry.disk_size, -1, TAG, "The address falls outside of the partition"); + + return (ssize_t)translated; +} + +static esp_err_t bd_gp_read(esp_blockdev_handle_t dev_handle, uint8_t *dst_buf, size_t dst_buf_size, uint64_t src_addr, size_t data_read_len) +{ + ESP_RETURN_ON_FALSE(dev_handle != NULL, ESP_ERR_INVALID_ARG, TAG, "The dev_handle cannot be NULL"); + ESP_RETURN_ON_FALSE(dst_buf != NULL, ESP_ERR_INVALID_ARG, TAG, "The destination buffer cannot be NULL"); + ESP_RETURN_ON_FALSE(data_read_len <= dst_buf_size, ESP_ERR_INVALID_SIZE, TAG, "Destination buffer too small"); + ESP_RETURN_ON_FALSE(src_addr + data_read_len <= dev_handle->geometry.disk_size, ESP_ERR_INVALID_ARG, TAG, "The address range falls outside of the disk"); + + esp_blockdev_generic_partition_t *dev = (esp_blockdev_generic_partition_t *)dev_handle; + esp_blockdev_handle_t parent = dev->parent; + assert(dev_handle->geometry.read_size > 0); + assert(src_addr % dev_handle->geometry.read_size == 0); + assert(data_read_len % dev_handle->geometry.read_size == 0); + + ssize_t addr_parent = esp_blockdev_generic_partition_translate_address_to_parent(dev_handle, src_addr); + ESP_RETURN_ON_FALSE(addr_parent >= 0, ESP_ERR_INVALID_ARG, TAG, "Failed to translate address"); + + return parent->ops->read(parent, dst_buf, dst_buf_size, (uint64_t)addr_parent, data_read_len); +} + +static esp_err_t bd_gp_write(esp_blockdev_handle_t dev_handle, const uint8_t* src_buf, uint64_t dst_addr, size_t data_write_len) +{ + ESP_RETURN_ON_FALSE(dev_handle != NULL, ESP_ERR_INVALID_ARG, TAG, "The dev_handle cannot be NULL"); + ESP_RETURN_ON_FALSE(src_buf != NULL, ESP_ERR_INVALID_ARG, TAG, "The source buffer cannot be NULL"); + ESP_RETURN_ON_FALSE(dst_addr + data_write_len <= dev_handle->geometry.disk_size, ESP_ERR_INVALID_ARG, TAG, "The address range falls outside of the disk"); + + esp_blockdev_generic_partition_t *dev = (esp_blockdev_generic_partition_t *)dev_handle; + esp_blockdev_handle_t parent = dev->parent; + /* + * These asserts are intentional: the contract makes violating these preconditions undefined, + * so returning dedicated error codes here is not strictly necessary. + * The underlying parent blockdev will most likely perform the same checks itself. + */ + assert(!dev_handle->device_flags.read_only); + assert(dev_handle->geometry.write_size > 0); + assert(dst_addr % dev_handle->geometry.write_size == 0); + assert(data_write_len % dev_handle->geometry.write_size == 0); + + ssize_t addr_parent = esp_blockdev_generic_partition_translate_address_to_parent(dev_handle, dst_addr); + ESP_RETURN_ON_FALSE(addr_parent >= 0, ESP_ERR_INVALID_ARG, TAG, "Failed to translate address"); + + return parent->ops->write(parent, src_buf, (uint64_t)addr_parent, data_write_len); +} + +static esp_err_t bd_gp_erase(esp_blockdev_handle_t dev_handle, uint64_t start_addr, size_t erase_len) +{ + ESP_RETURN_ON_FALSE(dev_handle != NULL, ESP_ERR_INVALID_ARG, TAG, "The dev_handle cannot be NULL"); + + esp_blockdev_generic_partition_t *dev = (esp_blockdev_generic_partition_t *)dev_handle; + esp_blockdev_handle_t parent = dev->parent; + assert(!dev_handle->device_flags.read_only); + assert(dev_handle->geometry.erase_size > 0); + assert(start_addr % dev_handle->geometry.erase_size == 0); + assert(erase_len % dev_handle->geometry.erase_size == 0); + + if (start_addr + erase_len > dev->dev.geometry.disk_size) { + return ESP_ERR_INVALID_ARG; + } + + ssize_t addr_parent = esp_blockdev_generic_partition_translate_address_to_parent(dev_handle, start_addr); + ESP_RETURN_ON_FALSE(addr_parent >= 0, ESP_ERR_INVALID_ARG, TAG, "Failed to translate address"); + + return parent->ops->erase(parent, (uint64_t)addr_parent, erase_len); +} + +static esp_err_t bd_gp_sync(esp_blockdev_handle_t dev_handle) +{ + ESP_RETURN_ON_FALSE(dev_handle != NULL, ESP_ERR_INVALID_ARG, TAG, "The dev_handle cannot be NULL"); + + esp_blockdev_generic_partition_t *dev = (esp_blockdev_generic_partition_t *)dev_handle; + esp_blockdev_handle_t parent = dev->parent; + + if (parent->ops->sync == NULL) { + return ESP_OK; + } + + return parent->ops->sync(parent); +} + +static esp_err_t bd_gp_ioctl(esp_blockdev_handle_t dev_handle, const uint8_t cmd, void *args) +{ + ESP_RETURN_ON_FALSE(dev_handle != NULL, ESP_ERR_INVALID_ARG, TAG, "The dev_handle cannot be NULL"); + + esp_blockdev_generic_partition_t *dev = (esp_blockdev_generic_partition_t *)dev_handle; + esp_blockdev_handle_t parent = dev->parent; + + if (cmd == ESP_BLOCKDEV_CMD_MARK_DELETED || cmd == ESP_BLOCKDEV_CMD_ERASE_CONTENTS) { + ESP_RETURN_ON_FALSE(args != NULL, ESP_ERR_INVALID_ARG, TAG, "The ioctl arguments cannot be NULL"); + + esp_blockdev_cmd_arg_erase_t *erase_args = (esp_blockdev_cmd_arg_erase_t *)args; + ESP_RETURN_ON_FALSE(erase_args->start_addr <= dev->dev.geometry.disk_size, + ESP_ERR_INVALID_ARG, TAG, "The address range falls outside of the disk"); + ESP_RETURN_ON_FALSE(erase_args->erase_len <= dev->dev.geometry.disk_size - erase_args->start_addr, + ESP_ERR_INVALID_ARG, TAG, "The address range falls outside of the disk"); + assert(dev->dev.geometry.erase_size > 0); + assert(erase_args->start_addr % dev->dev.geometry.erase_size == 0); + assert(erase_args->erase_len % dev->dev.geometry.erase_size == 0); + + ssize_t addr_parent = esp_blockdev_generic_partition_translate_address_to_parent(dev_handle, erase_args->start_addr); + ESP_RETURN_ON_FALSE(addr_parent >= 0, ESP_ERR_INVALID_ARG, TAG, "Failed to translate address"); + + esp_blockdev_cmd_arg_erase_t translated_args = *erase_args; + translated_args.start_addr = (uint64_t)addr_parent; + + ESP_RETURN_ON_FALSE(parent->ops->ioctl != NULL, ESP_ERR_NOT_SUPPORTED, TAG, "Parent device does not implement ioctl"); + return parent->ops->ioctl(parent, cmd, &translated_args); + } + + ESP_RETURN_ON_FALSE(parent->ops->ioctl != NULL, ESP_ERR_NOT_SUPPORTED, TAG, "Parent device does not implement ioctl"); + return parent->ops->ioctl(parent, cmd, args); +} + +static esp_err_t bd_gp_release(esp_blockdev_handle_t dev_handle) +{ + ESP_RETURN_ON_FALSE(dev_handle != NULL, ESP_ERR_INVALID_ARG, TAG, "The dev_handle cannot be NULL"); + + free(dev_handle); + + return ESP_OK; +} + +static const esp_blockdev_ops_t g_generic_partition_ops = { + .read = bd_gp_read, + .write = bd_gp_write, + .erase = bd_gp_erase, + .sync = bd_gp_sync, + .ioctl = bd_gp_ioctl, + .release = bd_gp_release, +}; + +esp_err_t esp_blockdev_generic_partition_get(esp_blockdev_handle_t parent, size_t start_offset, size_t size, esp_blockdev_handle_t *out) +{ + ESP_RETURN_ON_FALSE(parent != NULL, ESP_ERR_INVALID_ARG, TAG, "The parent device handle cannot be NULL"); + ESP_RETURN_ON_FALSE(out != NULL, ESP_ERR_INVALID_ARG, TAG, "The out pointer cannot be NULL"); + *out = ESP_BLOCKDEV_HANDLE_INVALID; + + uint64_t partition_end = (uint64_t)start_offset + (uint64_t)size; + ESP_RETURN_ON_FALSE(partition_end >= (uint64_t)start_offset, ESP_ERR_INVALID_ARG, TAG, "The requested range overflows"); + ESP_RETURN_ON_FALSE(partition_end <= parent->geometry.disk_size, ESP_ERR_INVALID_ARG, TAG, "The address range falls outside of the disk"); + + esp_blockdev_generic_partition_t *part = calloc(1, sizeof(esp_blockdev_generic_partition_t)); + + if (part == NULL) { + return ESP_ERR_NO_MEM; + } + + *part = (esp_blockdev_generic_partition_t) { + .dev = { + .device_flags = parent->device_flags, + .geometry = { + .disk_size = size, + .read_size = parent->geometry.read_size, + .write_size = parent->geometry.write_size, + .erase_size = parent->geometry.erase_size, + .recommended_write_size = parent->geometry.recommended_write_size, + .recommended_read_size = parent->geometry.recommended_read_size, + .recommended_erase_size = parent->geometry.recommended_erase_size, + }, + .ops = &g_generic_partition_ops, + }, + .parent = parent, + .start_offset = start_offset, + }; + + part->dev.ctx = part; + + *out = (esp_blockdev_handle_t) part; + + return ESP_OK; +} diff --git a/components/esp_blockdev_util/include/esp_blockdev/generic_partition.h b/components/esp_blockdev_util/include/esp_blockdev/generic_partition.h new file mode 100644 index 0000000000..f09899e265 --- /dev/null +++ b/components/esp_blockdev_util/include/esp_blockdev/generic_partition.h @@ -0,0 +1,55 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#include +#include + +#include "esp_blockdev.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Create a new partition over a given blockdev + * + * @param parent The underlying device + * @param start Address of the start of the new partition. (Behaviour is undefined when this is not a multiple of block size for underlying device) + * @param size Size of the new partition. (Behaviour is undefined when this is not a multiple of block size for underlying device) + * @param out Where to store handle to the newly created block device. Will be unchanged upon failure. + * + * @return ESP_ERR_INVALID_ARG - Invalid argument was passed + * ESP_ERR_NO_MEM - Failed to allocate the struct + * ESP_OK + */ +esp_err_t esp_blockdev_generic_partition_get(esp_blockdev_handle_t parent, size_t start, size_t size, esp_blockdev_handle_t *out); + +/** + * @brief Translate virtual partition address to parents address space + * + * @param device The device for which to do the translation + * @param address Address to be translated, relative to the partition start + * + * @return Parent-space address on success + * -1 on error (invalid argument or overflow) + */ +ssize_t esp_blockdev_generic_partition_translate_address_to_parent(esp_blockdev_handle_t device, size_t address); + +/** + * @brief Translate virtual partition address from parents address space + * + * @param device The device for which to do the translation + * @param address Address to be translated, in the parent address space + * + * @return Partition-relative address on success + * -1 on error (invalid argument or underflow) + */ +ssize_t esp_blockdev_generic_partition_translate_address_to_child(esp_blockdev_handle_t device, size_t address); + +#ifdef __cplusplus +} +#endif diff --git a/components/esp_blockdev_util/test_apps/.build-test-rules.yml b/components/esp_blockdev_util/test_apps/.build-test-rules.yml index 0e99fb4bbc..cd45b13d67 100644 --- a/components/esp_blockdev_util/test_apps/.build-test-rules.yml +++ b/components/esp_blockdev_util/test_apps/.build-test-rules.yml @@ -1,5 +1,12 @@ # Documentation: .gitlab/ci/README.md#manifest-file-to-control-the-buildtest-apps +components/esp_blockdev_util/test_apps/generic_partition: + enable: + - if: INCLUDE_DEFAULT == 1 or IDF_TARGET == "linux" + disable_test: + - if: IDF_TARGET not in ["esp32", "esp32c3", "linux"] + temporary: true + reason: cover Xtensa and RISC-V targets components/esp_blockdev_util/test_apps/memory_blockdev: enable: - if: INCLUDE_DEFAULT == 1 or IDF_TARGET == "linux" diff --git a/components/esp_blockdev_util/test_apps/generic_partition/CMakeLists.txt b/components/esp_blockdev_util/test_apps/generic_partition/CMakeLists.txt new file mode 100644 index 0000000000..ca8a112637 --- /dev/null +++ b/components/esp_blockdev_util/test_apps/generic_partition/CMakeLists.txt @@ -0,0 +1,11 @@ +# This is the project CMakeLists.txt file for the generic partition test application +cmake_minimum_required(VERSION 3.22) + +set(EXTRA_COMPONENT_DIRS + "$ENV{IDF_PATH}/tools/test_apps/components" + "${CMAKE_CURRENT_LIST_DIR}/../../" + "${CMAKE_CURRENT_LIST_DIR}/../../../esp_blockdev") + +set(COMPONENTS main) +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(generic_partition_test) diff --git a/components/esp_blockdev_util/test_apps/generic_partition/README.md b/components/esp_blockdev_util/test_apps/generic_partition/README.md new file mode 100644 index 0000000000..691f9018de --- /dev/null +++ b/components/esp_blockdev_util/test_apps/generic_partition/README.md @@ -0,0 +1,2 @@ +| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C5 | ESP32-C6 | ESP32-C61 | ESP32-H2 | ESP32-H21 | ESP32-H4 | ESP32-P4 | ESP32-S2 | ESP32-S3 | ESP32-S31 | Linux | +| ----------------- | ----- | -------- | -------- | -------- | -------- | --------- | -------- | --------- | -------- | -------- | -------- | -------- | --------- | ----- | diff --git a/components/esp_blockdev_util/test_apps/generic_partition/main/CMakeLists.txt b/components/esp_blockdev_util/test_apps/generic_partition/main/CMakeLists.txt new file mode 100644 index 0000000000..d338df6bb2 --- /dev/null +++ b/components/esp_blockdev_util/test_apps/generic_partition/main/CMakeLists.txt @@ -0,0 +1,2 @@ +idf_component_register(SRCS "test_generic_partition.c" + PRIV_REQUIRES unity esp_blockdev esp_blockdev_util) diff --git a/components/esp_blockdev_util/test_apps/generic_partition/main/test_generic_partition.c b/components/esp_blockdev_util/test_apps/generic_partition/main/test_generic_partition.c new file mode 100644 index 0000000000..04124e11bc --- /dev/null +++ b/components/esp_blockdev_util/test_apps/generic_partition/main/test_generic_partition.c @@ -0,0 +1,371 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include + +#include "unity.h" +#include "unity_test_utils.h" + +#include "esp_blockdev.h" +#include "esp_blockdev/generic_partition.h" +#include "esp_blockdev/memory.h" + +static esp_blockdev_handle_t create_memory_parent(uint8_t *backing, size_t backing_size) +{ + const esp_blockdev_geometry_t geometry = { + .disk_size = backing_size, + .read_size = 1, + .write_size = 1, + .erase_size = 1, + .recommended_write_size = 8, + .recommended_read_size = 8, + .recommended_erase_size = 16, + }; + + esp_blockdev_handle_t parent = NULL; + TEST_ESP_OK(esp_blockdev_memory_get_from_buffer(backing, backing_size, &geometry, false, &parent)); + TEST_ASSERT_NOT_NULL(parent); + + return parent; +} + +TEST_CASE("generic partition basic read/write/erase", "[generic_partition]") +{ + uint8_t backing[256]; + memset(backing, 0xEE, sizeof(backing)); + + esp_blockdev_handle_t parent = create_memory_parent(backing, sizeof(backing)); + + const size_t partition_offset = 64; + const size_t partition_size = 128; + + esp_blockdev_handle_t part = NULL; + TEST_ESP_OK(esp_blockdev_generic_partition_get(parent, partition_offset, partition_size, &part)); + TEST_ASSERT_NOT_NULL(part); + + TEST_ASSERT_EQUAL_UINT64(partition_size, part->geometry.disk_size); + TEST_ASSERT_EQUAL_UINT32(parent->geometry.read_size, part->geometry.read_size); + TEST_ASSERT_EQUAL_UINT32(parent->geometry.write_size, part->geometry.write_size); + TEST_ASSERT_EQUAL_UINT32(parent->geometry.erase_size, part->geometry.erase_size); + + uint8_t pattern[32]; + for (size_t i = 0; i < sizeof(pattern); ++i) { + pattern[i] = (uint8_t)(i ^ 0xA5); + } + + const uint64_t write_addr = 48; + TEST_ESP_OK(part->ops->write(part, pattern, write_addr, sizeof(pattern))); + TEST_ASSERT_EQUAL_UINT8_ARRAY(pattern, &backing[partition_offset + write_addr], sizeof(pattern)); + + uint8_t read_buf[sizeof(pattern)]; + memset(read_buf, 0, sizeof(read_buf)); + TEST_ESP_OK(part->ops->read(part, read_buf, sizeof(read_buf), write_addr, sizeof(read_buf))); + TEST_ASSERT_EQUAL_UINT8_ARRAY(pattern, read_buf, sizeof(pattern)); + + TEST_ESP_OK(part->ops->erase(part, write_addr, sizeof(pattern))); + TEST_ASSERT_EACH_EQUAL_UINT8(0x00, &backing[partition_offset + write_addr], sizeof(pattern)); + + TEST_ESP_OK(part->ops->release(part)); + TEST_ESP_OK(parent->ops->release(parent)); +} + +TEST_CASE("generic partition translate helpers", "[generic_partition]") +{ + uint8_t backing[32]; + memset(backing, 0x00, sizeof(backing)); + + esp_blockdev_handle_t parent = create_memory_parent(backing, sizeof(backing)); + + esp_blockdev_handle_t part = NULL; + TEST_ESP_OK(esp_blockdev_generic_partition_get(parent, 4, 16, &part)); + + ssize_t parent_addr = esp_blockdev_generic_partition_translate_address_to_parent(part, 3); + TEST_ASSERT_GREATER_OR_EQUAL(0, parent_addr); + TEST_ASSERT_EQUAL_INT(7, parent_addr); + + ssize_t child_addr = esp_blockdev_generic_partition_translate_address_to_child(part, 10); + TEST_ASSERT_GREATER_OR_EQUAL(0, child_addr); + TEST_ASSERT_EQUAL_INT(6, child_addr); + + TEST_ASSERT_EQUAL_INT(-1, esp_blockdev_generic_partition_translate_address_to_parent(part, 20)); + TEST_ASSERT_EQUAL_INT(-1, esp_blockdev_generic_partition_translate_address_to_child(part, 2)); + + TEST_ESP_OK(part->ops->release(part)); + TEST_ESP_OK(parent->ops->release(parent)); +} + +typedef struct { + esp_blockdev_t dev; + struct { + uint8_t *dst_buf; + size_t dst_buf_size; + uint64_t addr; + size_t len; + bool called; + } read; + struct { + const uint8_t *src_buf; + uint64_t addr; + size_t len; + bool called; + } write; + struct { + uint64_t addr; + size_t len; + bool called; + } erase; + bool sync_called; + struct { + uint8_t cmd; + void *args; + esp_blockdev_cmd_arg_erase_t erase_arg; + bool erase_arg_copied; + bool called; + } ioctl; +} mock_blockdev_t; + +static esp_err_t mock_read(esp_blockdev_handle_t dev, uint8_t *dst_buf, size_t dst_buf_size, uint64_t src_addr, size_t data_len) +{ + mock_blockdev_t *mock = (mock_blockdev_t *)dev->ctx; + mock->read.called = true; + mock->read.dst_buf = dst_buf; + mock->read.dst_buf_size = dst_buf_size; + mock->read.addr = src_addr; + mock->read.len = data_len; + return ESP_OK; +} + +static esp_err_t mock_write(esp_blockdev_handle_t dev, const uint8_t *src_buf, uint64_t dst_addr, size_t data_len) +{ + mock_blockdev_t *mock = (mock_blockdev_t *)dev->ctx; + mock->write.called = true; + mock->write.src_buf = src_buf; + mock->write.addr = dst_addr; + mock->write.len = data_len; + return ESP_OK; +} + +static esp_err_t mock_erase(esp_blockdev_handle_t dev, uint64_t start_addr, size_t erase_len) +{ + mock_blockdev_t *mock = (mock_blockdev_t *)dev->ctx; + mock->erase.called = true; + mock->erase.addr = start_addr; + mock->erase.len = erase_len; + return ESP_OK; +} + +static esp_err_t mock_sync(esp_blockdev_handle_t dev) +{ + mock_blockdev_t *mock = (mock_blockdev_t *)dev->ctx; + mock->sync_called = true; + return ESP_OK; +} + +static esp_err_t mock_ioctl(esp_blockdev_handle_t dev, const uint8_t cmd, void *args) +{ + mock_blockdev_t *mock = (mock_blockdev_t *)dev->ctx; + mock->ioctl.called = true; + mock->ioctl.cmd = cmd; + mock->ioctl.args = args; + if ((cmd == ESP_BLOCKDEV_CMD_MARK_DELETED || cmd == ESP_BLOCKDEV_CMD_ERASE_CONTENTS) && args != NULL) { + mock->ioctl.erase_arg = *(esp_blockdev_cmd_arg_erase_t *)args; + mock->ioctl.erase_arg_copied = true; + } + return ESP_OK; +} + +static esp_err_t mock_release(esp_blockdev_handle_t dev) +{ + mock_blockdev_t *mock = (mock_blockdev_t *)dev->ctx; + memset(mock, 0, sizeof(*mock)); + return ESP_OK; +} + +static const esp_blockdev_ops_t MOCK_OPS = { + .read = mock_read, + .write = mock_write, + .erase = mock_erase, + .sync = mock_sync, + .ioctl = mock_ioctl, + .release = mock_release, +}; + +static const esp_blockdev_ops_t MOCK_OPS_NO_SYNC_IOCTL = { + .read = mock_read, + .write = mock_write, + .erase = mock_erase, + .sync = NULL, + .ioctl = NULL, + .release = mock_release, +}; + +static void init_mock_with_ops(mock_blockdev_t *mock, uint64_t disk_size, const esp_blockdev_ops_t *ops); + +static void init_mock(mock_blockdev_t *mock, uint64_t disk_size) +{ + init_mock_with_ops(mock, disk_size, &MOCK_OPS); +} + +static void init_mock_with_ops(mock_blockdev_t *mock, uint64_t disk_size, const esp_blockdev_ops_t *ops) +{ + memset(mock, 0, sizeof(*mock)); + mock->dev.ctx = mock; + mock->dev.device_flags.val = 0; + mock->dev.geometry = (esp_blockdev_geometry_t) { + .disk_size = disk_size, + .read_size = 4, + .write_size = 4, + .erase_size = 16, + .recommended_read_size = 8, + .recommended_write_size = 8, + .recommended_erase_size = 64, + }; + mock->dev.ops = ops; +} + +TEST_CASE("generic partition forwards operations to parent", "[generic_partition]") +{ + mock_blockdev_t mock_parent; + init_mock(&mock_parent, 1024); + + esp_blockdev_handle_t part = NULL; + const size_t start = 128; + const size_t size = 512; + + TEST_ESP_OK(esp_blockdev_generic_partition_get((esp_blockdev_handle_t)&mock_parent, start, size, &part)); + + uint8_t buffer[64]; + uint8_t src[32]; + memset(src, 0xCC, sizeof(src)); + + TEST_ESP_OK(part->ops->read(part, buffer, sizeof(buffer), 12, sizeof(buffer))); + TEST_ASSERT_TRUE(mock_parent.read.called); + TEST_ASSERT_EQUAL_PTR(buffer, mock_parent.read.dst_buf); + TEST_ASSERT_EQUAL_UINT32(sizeof(buffer), mock_parent.read.dst_buf_size); + TEST_ASSERT_EQUAL_UINT64(start + 12, mock_parent.read.addr); + TEST_ASSERT_EQUAL_UINT32(sizeof(buffer), mock_parent.read.len); + + TEST_ESP_OK(part->ops->write(part, src, 24, sizeof(src))); + TEST_ASSERT_TRUE(mock_parent.write.called); + TEST_ASSERT_EQUAL_PTR(src, mock_parent.write.src_buf); + TEST_ASSERT_EQUAL_UINT64(start + 24, mock_parent.write.addr); + TEST_ASSERT_EQUAL_UINT32(sizeof(src), mock_parent.write.len); + + TEST_ESP_OK(part->ops->erase(part, 48, 96)); + TEST_ASSERT_TRUE(mock_parent.erase.called); + TEST_ASSERT_EQUAL_UINT64(start + 48, mock_parent.erase.addr); + TEST_ASSERT_EQUAL_UINT32(96, mock_parent.erase.len); + + TEST_ESP_OK(part->ops->sync(part)); + TEST_ASSERT_TRUE(mock_parent.sync_called); + + uint32_t ioctl_arg = 0xDEADBEEF; + TEST_ESP_OK(part->ops->ioctl(part, 0x42, &ioctl_arg)); + TEST_ASSERT_TRUE(mock_parent.ioctl.called); + TEST_ASSERT_EQUAL_UINT8(0x42, mock_parent.ioctl.cmd); + TEST_ASSERT_EQUAL_PTR(&ioctl_arg, mock_parent.ioctl.args); + + TEST_ESP_OK(part->ops->release(part)); + TEST_ESP_OK(mock_parent.dev.ops->release((esp_blockdev_handle_t)&mock_parent)); +} + +TEST_CASE("generic partition remaps mark deleted ioctl addresses", "[generic_partition]") +{ + mock_blockdev_t mock_parent; + init_mock(&mock_parent, 1024); + + esp_blockdev_handle_t part = NULL; + const size_t start = 128; + const size_t size = 512; + + TEST_ESP_OK(esp_blockdev_generic_partition_get((esp_blockdev_handle_t)&mock_parent, start, size, &part)); + + esp_blockdev_cmd_arg_erase_t mark_deleted = { + .start_addr = 48, + .erase_len = 96, + }; + + TEST_ESP_OK(part->ops->ioctl(part, ESP_BLOCKDEV_CMD_MARK_DELETED, &mark_deleted)); + TEST_ASSERT_TRUE(mock_parent.ioctl.called); + TEST_ASSERT_EQUAL_UINT8(ESP_BLOCKDEV_CMD_MARK_DELETED, mock_parent.ioctl.cmd); + TEST_ASSERT_TRUE(mock_parent.ioctl.erase_arg_copied); + TEST_ASSERT_EQUAL_UINT64(start + 48, mock_parent.ioctl.erase_arg.start_addr); + TEST_ASSERT_EQUAL_UINT32(96, mock_parent.ioctl.erase_arg.erase_len); + TEST_ASSERT_EQUAL_UINT64(48, mark_deleted.start_addr); + TEST_ASSERT_EQUAL_UINT32(96, mark_deleted.erase_len); + + TEST_ESP_OK(part->ops->release(part)); + TEST_ESP_OK(mock_parent.dev.ops->release((esp_blockdev_handle_t)&mock_parent)); +} + +TEST_CASE("generic partition remaps erase contents ioctl addresses", "[generic_partition]") +{ + mock_blockdev_t mock_parent; + init_mock(&mock_parent, 1024); + + esp_blockdev_handle_t part = NULL; + const size_t start = 128; + const size_t size = 512; + + TEST_ESP_OK(esp_blockdev_generic_partition_get((esp_blockdev_handle_t)&mock_parent, start, size, &part)); + + esp_blockdev_cmd_arg_erase_t erase_contents = { + .start_addr = 64, + .erase_len = 96, + }; + + TEST_ESP_OK(part->ops->ioctl(part, ESP_BLOCKDEV_CMD_ERASE_CONTENTS, &erase_contents)); + TEST_ASSERT_TRUE(mock_parent.ioctl.called); + TEST_ASSERT_EQUAL_UINT8(ESP_BLOCKDEV_CMD_ERASE_CONTENTS, mock_parent.ioctl.cmd); + TEST_ASSERT_TRUE(mock_parent.ioctl.erase_arg_copied); + TEST_ASSERT_EQUAL_UINT64(start + 64, mock_parent.ioctl.erase_arg.start_addr); + TEST_ASSERT_EQUAL_UINT32(96, mock_parent.ioctl.erase_arg.erase_len); + TEST_ASSERT_EQUAL_UINT64(64, erase_contents.start_addr); + TEST_ASSERT_EQUAL_UINT32(96, erase_contents.erase_len); + + TEST_ESP_OK(part->ops->release(part)); + TEST_ESP_OK(mock_parent.dev.ops->release((esp_blockdev_handle_t)&mock_parent)); +} + +TEST_CASE("generic partition tolerates missing optional parent ops", "[generic_partition]") +{ + mock_blockdev_t mock_parent; + init_mock_with_ops(&mock_parent, 1024, &MOCK_OPS_NO_SYNC_IOCTL); + + esp_blockdev_handle_t part = NULL; + TEST_ESP_OK(esp_blockdev_generic_partition_get((esp_blockdev_handle_t)&mock_parent, 128, 512, &part)); + + TEST_ESP_OK(part->ops->sync(part)); + TEST_ASSERT_EQUAL(ESP_ERR_NOT_SUPPORTED, part->ops->ioctl(part, 0x44, NULL)); + + TEST_ESP_OK(part->ops->release(part)); + TEST_ESP_OK(mock_parent.dev.ops->release((esp_blockdev_handle_t)&mock_parent)); +} + +TEST_CASE("generic partition creation validation", "[generic_partition]") +{ + uint8_t backing[128]; + memset(backing, 0, sizeof(backing)); + esp_blockdev_handle_t parent = create_memory_parent(backing, sizeof(backing)); + + esp_blockdev_handle_t part = (esp_blockdev_handle_t)0xDEADBEEF; + TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, esp_blockdev_generic_partition_get(parent, 64, 256, &part)); + TEST_ASSERT_EQUAL_PTR(NULL, part); + + part = (esp_blockdev_handle_t)0xDEADBEEF; + TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, esp_blockdev_generic_partition_get(parent, SIZE_MAX, 64, &part)); + TEST_ASSERT_EQUAL_PTR(NULL, part); + + TEST_ESP_OK(parent->ops->release(parent)); +} + +void app_main(void) +{ + unity_run_menu(); +} diff --git a/components/esp_blockdev_util/test_apps/generic_partition/pytest_generic_partition.py b/components/esp_blockdev_util/test_apps/generic_partition/pytest_generic_partition.py new file mode 100644 index 0000000000..28e3bfbeca --- /dev/null +++ b/components/esp_blockdev_util/test_apps/generic_partition/pytest_generic_partition.py @@ -0,0 +1,11 @@ +# SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: CC0-1.0 +import pytest +from pytest_embedded import Dut +from pytest_embedded_idf.utils import idf_parametrize + + +@pytest.mark.generic +@idf_parametrize('target', ['esp32', 'esp32c3', 'linux'], indirect=['target']) +def test_generic_partition(dut: Dut) -> None: + dut.run_all_single_board_cases() diff --git a/components/esp_blockdev_util/test_apps/generic_partition/sdkconfig.defaults b/components/esp_blockdev_util/test_apps/generic_partition/sdkconfig.defaults new file mode 100644 index 0000000000..ae3f2121da --- /dev/null +++ b/components/esp_blockdev_util/test_apps/generic_partition/sdkconfig.defaults @@ -0,0 +1 @@ +CONFIG_UNITY_ENABLE_64BIT=y