Extend storage volume for fw update installation

To install update images into storage, a flash storage volume needs
to be erased prior to streamed write operations to install the new
image. This change extends volume support to add an erase operation
that can be implemented by a concrete volume. Block volume tests
have been extended to include a test scenario that mimics multiple
image install operations. Also adds an optional method to get
GUIDs related to volume storage to facilitate populating the
FWU fw_directory.

Signed-off-by: Julian Hall <julian.hall@arm.com>
Change-Id: I3ba4dea6d0ed82d6bb54066c7df97c5d6d7d4c87
diff --git a/components/media/disk/formatter/disk_formatter.c b/components/media/disk/formatter/disk_formatter.c
index 6b6d146..1d8e2fe 100644
--- a/components/media/disk/formatter/disk_formatter.c
+++ b/components/media/disk/formatter/disk_formatter.c
@@ -10,14 +10,14 @@
 
 int disk_formatter_clone(
 	uintptr_t dev_handle,
-	uintptr_t volume_spec,
+	uintptr_t io_spec,
 	const uint8_t *source_image,
 	size_t source_image_size)
 {
 	uintptr_t volume_handle;
 	int result;
 
-	result = io_open(dev_handle, volume_spec, &volume_handle);
+	result = io_open(dev_handle, io_spec, &volume_handle);
 	if (result != 0)
 		return result;
 
diff --git a/components/media/disk/formatter/disk_formatter.h b/components/media/disk/formatter/disk_formatter.h
index 95b058e..e461d9a 100644
--- a/components/media/disk/formatter/disk_formatter.h
+++ b/components/media/disk/formatter/disk_formatter.h
@@ -18,7 +18,7 @@
  * @brief  Format a storage volume by cloning a disk image
  *
  * @param[in] dev_handle    IO device handle
- * @param[in] volume_spec   Opaque volume spec
+ * @param[in] io_spec   Opaque volume spec
  * @param[in] source_image  The source disk image to clone
  * @param[in] source_image_size  The size of the source image
  *
@@ -26,7 +26,7 @@
  */
 int disk_formatter_clone(
 	uintptr_t dev_handle,
-	uintptr_t volume_spec,
+	uintptr_t io_spec,
 	const uint8_t *source_image,
 	size_t source_image_size);
 
diff --git a/components/media/disk/test/partition_table_tests.cpp b/components/media/disk/test/partition_table_tests.cpp
index 9a38a99..29cf0dd 100644
--- a/components/media/disk/test/partition_table_tests.cpp
+++ b/components/media/disk/test/partition_table_tests.cpp
@@ -11,7 +11,7 @@
 #include <service/block_storage/block_store/device/ram/ram_block_store.h>
 #include <service/block_storage/config/ref/ref_partition_configurator.h>
 #include <media/volume/index/volume_index.h>
-#include <media/volume/block_io_dev/block_io_dev.h>
+#include <media/volume/block_volume/block_volume.h>
 #include <media/disk/disk_images/ref_partition.h>
 #include <media/disk/formatter/disk_formatter.h>
 #include <media/disk/partition_table.h>
@@ -33,30 +33,28 @@
 
 		memset(m_partition_guid.octets, 0, sizeof(m_partition_guid.octets));
 
-		m_dev_handle = 0;
-		m_volume_spec = 0;
+		m_volume = NULL;
 
-		int result = block_io_dev_init(&m_block_io_dev,
+		int result = block_volume_init(&m_block_volume,
 			m_block_store, &m_partition_guid,
-			&m_dev_handle, &m_volume_spec);
+			&m_volume);
 
 		LONGS_EQUAL(0, result);
-		CHECK_TRUE(m_dev_handle);
-		CHECK_TRUE(m_volume_spec);
+		CHECK_TRUE(m_volume);
 
 		result = disk_formatter_clone(
-			m_dev_handle, m_volume_spec,
+			m_volume->dev_handle, m_volume->io_spec,
 			ref_partition_data, ref_partition_data_length);
 
 		LONGS_EQUAL(0, result);
 
 		volume_index_init();
-		volume_index_add(VOLUME_ID_SECURE_FLASH, m_dev_handle, m_volume_spec);
+		volume_index_add(VOLUME_ID_SECURE_FLASH, m_volume);
 	}
 
 	void teardown()
 	{
-		block_io_dev_deinit(&m_block_io_dev);
+		block_volume_deinit(&m_block_volume);
 		ram_block_store_deinit(&m_ram_block_store);
 		volume_index_clear();
 	}
@@ -92,9 +90,8 @@
 	struct uuid_octets m_partition_guid;
 	struct block_store *m_block_store;
 	struct ram_block_store m_ram_block_store;
-	struct block_io_dev m_block_io_dev;
-	uintptr_t m_dev_handle;
-	uintptr_t m_volume_spec;
+	struct block_volume m_block_volume;
+	struct volume *m_volume;
 };
 
 TEST(PartitionTableTests, loadRefPartitionTable)
diff --git a/components/media/volume/block_io_dev/block_io_dev.h b/components/media/volume/block_io_dev/block_io_dev.h
deleted file mode 100644
index 37c91ed..0000000
--- a/components/media/volume/block_io_dev/block_io_dev.h
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * Copyright (c) 2022, Arm Limited and Contributors. All rights reserved.
- *
- * SPDX-License-Identifier: BSD-3-Clause
- */
-
-#ifndef MEDIA_BLOCK_IO_DEV_H
-#define MEDIA_BLOCK_IO_DEV_H
-
-#include <common/uuid/uuid.h>
-#include <service/block_storage/block_store/block_store.h>
-
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-/**
- * Export tf-a version with C++ linkage support.
- */
-#include <drivers/io/io_driver.h>
-
-/**
- * Provides a tf-a compatible io device that presents a block storage partition
- * as a single volume. Access to the underlying storage is handled by an associated
- * block_store. The block_store could be any concrete block_store.
- */
-struct block_io_dev
-{
-	io_dev_info_t dev_info;
-	size_t file_pos;
-	size_t size;
-	struct block_store *block_store;
-	struct uuid_octets partition_guid;
-	storage_partition_handle_t partition_handle;
-	struct storage_partition_info partition_info;
-};
-
-/**
- * @brief  Initialize an block_io_dev instance
- *
- * @param[in] this_instance    The subject block_io_dev
- * @param[in] block_store      The associated block_store
- * @param[in] partition_guid   The partition GUID
- * @param[out] dev_handle	   Device handle used by tf-a components
- * @param[out] spec			   Spec passed on io_open
- *
- * @return 0 on success
- */
-int block_io_dev_init(
-	struct block_io_dev *this_instance,
-	struct block_store *block_store,
-	const struct uuid_octets *partition_guid,
-	uintptr_t *dev_handle,
-	uintptr_t *spec);
-
-/**
- * @brief  De-initialize an block_io_dev instance
- *
- * @param[in] this_instance    The subject block_io_dev
- */
-void block_io_dev_deinit(
-	struct block_io_dev *this_instance);
-
-/**
- * @brief  Set the partition GUID
- *
- * Modifies the partition GUID. This will be used to identify the target
- * storage partition on a subsequent call to io_dev_open.
- *
- * @param[in] this_instance    The subject block_io_dev
- * @param[in] partition_guid   The partition GUID
- */
-void block_io_dev_set_partition_guid(
-	struct block_io_dev *this_instance,
-	const struct uuid_octets *partition_guid);
-
-#ifdef __cplusplus
-}
-#endif
-
-#endif /* MEDIA_BLOCK_IO_DEV_H */
diff --git a/components/media/volume/block_io_dev/test/block_io_dev_tests.cpp b/components/media/volume/block_io_dev/test/block_io_dev_tests.cpp
deleted file mode 100644
index 33940c9..0000000
--- a/components/media/volume/block_io_dev/test/block_io_dev_tests.cpp
+++ /dev/null
@@ -1,201 +0,0 @@
-/*
- * Copyright (c) 2022-2023, Arm Limited and Contributors. All rights reserved.
- *
- * SPDX-License-Identifier: BSD-3-Clause
- */
-
-
-#include <string>
-#include <cstring>
-#include <common/uuid/uuid.h>
-#include <service/block_storage/block_store/device/ram/ram_block_store.h>
-#include <media/volume/block_io_dev/block_io_dev.h>
-#include <media/volume/index/volume_index.h>
-#include <media/volume/base_io_dev/base_io_dev.h>
-#include <CppUTest/TestHarness.h>
-
-TEST_GROUP(BlockIoDevTests)
-{
-	void setup()
-	{
-		uuid_guid_octets_from_canonical(&m_partition_guid,
-			"6152f22b-8128-4c1f-981f-3bd279519907");
-
-		m_block_store = ram_block_store_init(&m_ram_block_store,
-			&m_partition_guid, NUM_BLOCKS, BLOCK_SIZE);
-
-		CHECK_TRUE(m_block_store);
-
-		m_dev_handle = 0;
-		m_volume_spec = 0;
-
-		int result = block_io_dev_init(&m_block_io_dev,
-			m_block_store, &m_partition_guid,
-			&m_dev_handle, &m_volume_spec);
-
-		LONGS_EQUAL(0, result);
-		CHECK_TRUE(m_dev_handle);
-		CHECK_TRUE(m_volume_spec);
-
-		volume_index_init();
-		volume_index_add(TEST_VOLUME_ID, m_dev_handle, m_volume_spec);
-	}
-
-	void teardown()
-	{
-		block_io_dev_deinit(&m_block_io_dev);
-		ram_block_store_deinit(&m_ram_block_store);
-		volume_index_clear();
-	}
-
-	static const unsigned int TEST_VOLUME_ID = 5;
-	static const size_t NUM_BLOCKS = 100;
-	static const size_t BLOCK_SIZE = 512;
-
-	struct uuid_octets m_partition_guid;
-	struct block_store *m_block_store;
-	struct ram_block_store m_ram_block_store;
-	struct block_io_dev m_block_io_dev;
-	uintptr_t m_dev_handle;
-	uintptr_t m_volume_spec;
-};
-
-
-TEST(BlockIoDevTests, openClose)
-{
-	/* Check the open flow used by tf-a components */
-	uintptr_t dev_handle = 0;
-	uintptr_t volume_spec = 0;
-	uintptr_t file_handle = 0;
-	int result;
-
-	result = plat_get_image_source(TEST_VOLUME_ID, &dev_handle, &volume_spec);
-	LONGS_EQUAL(0, result);
-	CHECK_TRUE(dev_handle);
-
-	result = io_open(dev_handle, volume_spec, &file_handle);
-	LONGS_EQUAL(0, result);
-	CHECK_TRUE(file_handle);
-
-	io_close(file_handle);
-}
-
-TEST(BlockIoDevTests, readAndWrite)
-{
-	uintptr_t file_handle = 0;
-	int result;
-
-	result = io_open(m_dev_handle, m_volume_spec, &file_handle);
-	LONGS_EQUAL(0, result);
-	CHECK_TRUE(file_handle);
-
-	std::string message("Oh what a beautiful mornin'");
-
-	/* Ensure writes cross a block boundary */
-	size_t num_iterations = BLOCK_SIZE / message.size() + 2;
-
-	/* Write message a few times. Expect file pointer to advance on each write */
-	for (size_t i = 0; i < num_iterations; ++i) {
-
-		size_t len_written = 0;
-
-		result = io_write(file_handle,
-			(const uintptr_t)message.c_str(), message.size(),
-			&len_written);
-
-		LONGS_EQUAL(0, result);
-		UNSIGNED_LONGS_EQUAL(message.size(), len_written);
-	}
-
-	result = io_seek(file_handle, IO_SEEK_SET, 0);
-	LONGS_EQUAL(0, result);
-
-	/* Expect to read back the same data */
-	uint8_t read_buf[message.size()];
-
-	for (size_t i = 0; i < num_iterations; ++i) {
-
-		size_t len_read = 0;
-
-		memset(read_buf, 0, sizeof(read_buf));
-
-		result = io_read(file_handle,
-			(const uintptr_t)read_buf, sizeof(read_buf),
-			&len_read);
-
-		LONGS_EQUAL(0, result);
-		UNSIGNED_LONGS_EQUAL(message.size(), len_read);
-		MEMCMP_EQUAL(message.c_str(), read_buf, message.size());
-	}
-
-	io_close(file_handle);
-}
-
-TEST(BlockIoDevTests, seekAccess)
-{
-	uintptr_t file_handle = 0;
-	size_t len = 0;
-	int result;
-
-	result = io_open(m_dev_handle, m_volume_spec, &file_handle);
-	LONGS_EQUAL(0, result);
-	CHECK_TRUE(file_handle);
-
-	std::string message("Knees up Mother Brown");
-
-	/* Initially seek to an arbitrary position around the middle of the volume */
-	size_t start_pos = (NUM_BLOCKS * BLOCK_SIZE) / 2 + 27;
-
-	/* Seek and write a few times */
-	result = io_seek(file_handle, IO_SEEK_SET, start_pos);
-	LONGS_EQUAL(0, result);
-
-	result = io_write(file_handle, (const uintptr_t)message.c_str(), message.size(), &len);
-	LONGS_EQUAL(0, result);
-	UNSIGNED_LONGS_EQUAL(message.size(), len);
-
-	/* Using IO_SEEK_SET, seek forward, skipping over the written message */
-	result = io_seek(file_handle, IO_SEEK_SET, start_pos + 110);
-	LONGS_EQUAL(0, result);
-
-	result = io_write(file_handle, (const uintptr_t)message.c_str(), message.size(), &len);
-	LONGS_EQUAL(0, result);
-	UNSIGNED_LONGS_EQUAL(message.size(), len);
-
-	/* Using IO_SEEK_CUR, seek forward again, far enough to skip over the message */
-	result = io_seek(file_handle, IO_SEEK_CUR, 715);
-	LONGS_EQUAL(0, result);
-
-	result = io_write(file_handle, (const uintptr_t)message.c_str(), message.size(), &len);
-	LONGS_EQUAL(0, result);
-	UNSIGNED_LONGS_EQUAL(message.size(), len);
-
-	/* Perform the same sequence of seeks and expect to read back intact copies of the message */
-	uint8_t read_buf[message.size()];
-
-	result = io_seek(file_handle, IO_SEEK_SET, start_pos);
-	LONGS_EQUAL(0, result);
-
-	result = io_read(file_handle, (uintptr_t)read_buf, sizeof(read_buf), &len);
-	LONGS_EQUAL(0, result);
-	UNSIGNED_LONGS_EQUAL(message.size(), len);
-	MEMCMP_EQUAL(message.c_str(), read_buf, message.size());
-
-	result = io_seek(file_handle, IO_SEEK_SET, start_pos + 110);
-	LONGS_EQUAL(0, result);
-
-	result = io_read(file_handle, (uintptr_t)read_buf, sizeof(read_buf), &len);
-	LONGS_EQUAL(0, result);
-	UNSIGNED_LONGS_EQUAL(message.size(), len);
-	MEMCMP_EQUAL(message.c_str(), read_buf, message.size());
-
-	result = io_seek(file_handle, IO_SEEK_CUR, 715);
-	LONGS_EQUAL(0, result);
-
-	result = io_read(file_handle, (uintptr_t)read_buf, sizeof(read_buf), &len);
-	LONGS_EQUAL(0, result);
-	UNSIGNED_LONGS_EQUAL(message.size(), len);
-	MEMCMP_EQUAL(message.c_str(), read_buf, message.size());
-
-	io_close(file_handle);
-}
\ No newline at end of file
diff --git a/components/media/volume/block_io_dev/block_io_dev.c b/components/media/volume/block_volume/block_volume.c
similarity index 62%
rename from components/media/volume/block_io_dev/block_io_dev.c
rename to components/media/volume/block_volume/block_volume.c
index 18bd003..137915c 100644
--- a/components/media/volume/block_io_dev/block_io_dev.c
+++ b/components/media/volume/block_volume/block_volume.c
@@ -5,45 +5,62 @@
  */
 
 #include <stddef.h>
+#include <stdint.h>
 #include <drivers/io/io_storage.h>
-#include "block_io_dev.h"
+#include "block_volume.h"
 
 /* Concrete io_dev interface functions */
-static io_type_t block_io_dev_type(
+static io_type_t block_volume_type(
 	void);
-static int block_io_dev_open(
+static int block_volume_open(
 	io_dev_info_t *dev_info, const uintptr_t spec, io_entity_t *entity);
-static int block_io_dev_close(
+static int block_volume_close(
 	io_entity_t *entity);
-static int block_io_dev_seek(
+static int block_volume_seek(
 	io_entity_t *entity, int mode, signed long long offset);
-static int block_io_dev_size(
+static int block_volume_size(
 	io_entity_t *entity, size_t *length);
-static int block_io_dev_read(
+static int block_volume_read(
 	io_entity_t *entity, uintptr_t buffer, size_t length, size_t *length_read);
-static int block_io_dev_write(
+static int block_volume_write(
 	io_entity_t *entity, const uintptr_t buffer, size_t length, size_t *length_written);
 
-static const io_dev_funcs_t block_io_dev_dev_funcs = {
-	.type		= block_io_dev_type,
-	.open		= block_io_dev_open,
-	.seek		= block_io_dev_seek,
-	.size		= block_io_dev_size,
-	.read		= block_io_dev_read,
-	.write		= block_io_dev_write,
-	.close		= block_io_dev_close,
+static const io_dev_funcs_t block_volume_dev_funcs = {
+	.type		= block_volume_type,
+	.open		= block_volume_open,
+	.seek		= block_volume_seek,
+	.size		= block_volume_size,
+	.read		= block_volume_read,
+	.write		= block_volume_write,
+	.close		= block_volume_close,
 	.dev_init	= NULL,
 	.dev_close	= NULL
 };
 
+/* Concrete volume functions that extend the io_dev interface */
+static int block_volume_erase(
+	uintptr_t context);
+static int block_volume_get_storage_ids(
+	uintptr_t context,
+	struct uuid_octets *partition_guid,
+	struct uuid_octets *parent_guid);
 
-int block_io_dev_init(
-	struct block_io_dev *this_instance,
+int block_volume_init(
+	struct block_volume *this_instance,
 	struct block_store *block_store,
 	const struct uuid_octets *partition_guid,
-	uintptr_t *dev_handle,
-	uintptr_t *spec)
+	struct volume **volume)
 {
+	/* Initialize base volume structure */
+	volume_init(
+		&this_instance->base_volume,
+		&block_volume_dev_funcs,
+		(uintptr_t)this_instance);
+
+	/* Initialize block_volume specific attributes */
+	this_instance->base_volume.erase = block_volume_erase;
+	this_instance->base_volume.get_storage_ids = block_volume_get_storage_ids;
+
 	this_instance->block_store = block_store;
 	this_instance->partition_guid = *partition_guid;
 
@@ -54,39 +71,35 @@
 	this_instance->partition_info.block_size = 0;
 	this_instance->partition_info.num_blocks = 0;
 
-	this_instance->dev_info.funcs = &block_io_dev_dev_funcs;
-	this_instance->dev_info.info = (uintptr_t)this_instance;
-
-	*dev_handle = (uintptr_t)&this_instance->dev_info;
-	*spec = (uintptr_t)this_instance;
+	*volume = &this_instance->base_volume;
 
 	return 0;
 }
 
-void block_io_dev_deinit(
-	struct block_io_dev *this_instance)
+void block_volume_deinit(
+	struct block_volume *this_instance)
 {
 	(void)this_instance;
 }
 
-void block_io_dev_set_partition_guid(
-	struct block_io_dev *this_instance,
+void block_volume_set_partition_guid(
+	struct block_volume *this_instance,
 	const struct uuid_octets *partition_guid)
 {
 	this_instance->partition_guid = *partition_guid;
 }
 
-static io_type_t block_io_dev_type(void)
+static io_type_t block_volume_type(void)
 {
 	return IO_TYPE_BLOCK;
 }
 
-static int block_io_dev_open(
+static int block_volume_open(
 	io_dev_info_t *dev_info,
 	const uintptr_t spec,
 	io_entity_t *entity)
 {
-	struct block_io_dev *this_instance = (struct block_io_dev*)dev_info->info;
+	struct block_volume *this_instance = (struct block_volume *)dev_info->info;
 	psa_status_t psa_status = PSA_ERROR_BAD_STATE;
 
 	psa_status = block_store_get_partition_info(this_instance->block_store,
@@ -109,10 +122,10 @@
 	return (psa_status == PSA_SUCCESS) ? 0 : -EPERM;
 }
 
-static int block_io_dev_close(
+static int block_volume_close(
 	io_entity_t *entity)
 {
-	struct block_io_dev *this_instance = (struct block_io_dev*)entity->info;
+	struct block_volume *this_instance = (struct block_volume *)entity->info;
 
 	psa_status_t psa_status = block_store_close(this_instance->block_store, 0,
 		this_instance->partition_handle);
@@ -126,12 +139,12 @@
 	return (psa_status == PSA_SUCCESS) ? 0 : -ENXIO;
 }
 
-static int block_io_dev_seek(
+static int block_volume_seek(
 	io_entity_t *entity,
 	int mode,
 	signed long long offset)
 {
-	struct block_io_dev *this_instance = (struct block_io_dev*)entity->info;
+	struct block_volume *this_instance = (struct block_volume *)entity->info;
 
 	switch (mode)
 	{
@@ -159,22 +172,22 @@
 	return 0;
 }
 
-static int block_io_dev_size(
+static int block_volume_size(
 	io_entity_t *entity,
 	size_t *length)
 {
-	struct block_io_dev *this_instance = (struct block_io_dev*)entity->info;
+	struct block_volume *this_instance = (struct block_volume *)entity->info;
 	*length = this_instance->size;
 	return 0;
 }
 
-static int block_io_dev_read(
+static int block_volume_read(
 	io_entity_t *entity,
 	uintptr_t buffer,
 	size_t length,
 	size_t *length_read)
 {
-	struct block_io_dev *this_instance = (struct block_io_dev*)entity->info;
+	struct block_volume *this_instance = (struct block_volume *)entity->info;
 	size_t bytes_read = 0;
 	*length_read = 0;
 
@@ -215,13 +228,13 @@
 	return 0;
 }
 
-static int block_io_dev_write(
+static int block_volume_write(
 	io_entity_t *entity,
 	const uintptr_t buffer,
 	size_t length,
 	size_t *length_written)
 {
-	struct block_io_dev *this_instance = (struct block_io_dev*)entity->info;
+	struct block_volume *this_instance = (struct block_volume *)entity->info;
 	size_t bytes_written = 0;
 	*length_written = 0;
 
@@ -261,3 +274,48 @@
 	*length_written = bytes_written;
 	return 0;
 }
+
+static int block_volume_erase(uintptr_t context)
+{
+	struct block_volume *this_instance = (struct block_volume *)context;
+
+	/* Erase the entire open partition. Note that a block_store will clip
+	 * the number of blocks to erase to the size of the partition so erasing
+	 * a large number of blocks is a safe way to erase the entire partition.
+	 */
+	psa_status_t psa_status = block_store_erase(
+		this_instance->block_store, 0,
+		this_instance->partition_handle,
+		0, UINT32_MAX);
+
+	if (psa_status != PSA_SUCCESS)
+		return -EIO;
+
+	return 0;
+}
+
+static int block_volume_get_storage_ids(
+	uintptr_t context,
+	struct uuid_octets *partition_guid,
+	struct uuid_octets *parent_guid)
+{
+	struct block_volume *this_instance = (struct block_volume *)context;
+	struct storage_partition_info partition_info;
+
+	psa_status_t psa_status = block_store_get_partition_info(this_instance->block_store,
+		&this_instance->partition_guid,
+		&partition_info);
+
+	if (psa_status == PSA_SUCCESS) {
+
+		if (partition_guid)
+			*partition_guid = partition_info.partition_guid;
+
+		if (parent_guid)
+			*parent_guid = partition_info.parent_guid;
+
+		return 0;
+	}
+
+	return -EINVAL;
+}
diff --git a/components/media/volume/block_volume/block_volume.h b/components/media/volume/block_volume/block_volume.h
new file mode 100644
index 0000000..8d3f2c8
--- /dev/null
+++ b/components/media/volume/block_volume/block_volume.h
@@ -0,0 +1,74 @@
+/*
+ * Copyright (c) 2022, Arm Limited and Contributors. All rights reserved.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+#ifndef MEDIA_BLOCK_VOLUME_H
+#define MEDIA_BLOCK_VOLUME_H
+
+#include <common/uuid/uuid.h>
+#include <media/volume/volume.h>
+#include <service/block_storage/block_store/block_store.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * Provides a concrete volume that presents a block storage partition
+ * as a single volume. Access to the underlying storage is handled by an associated
+ * block_store. The block_store could be any concrete block_store.
+ */
+struct block_volume {
+	struct volume base_volume;
+	size_t file_pos;
+	size_t size;
+	struct block_store *block_store;
+	struct uuid_octets partition_guid;
+	storage_partition_handle_t partition_handle;
+	struct storage_partition_info partition_info;
+};
+
+/**
+ * @brief  Initialize an block_volume instance
+ *
+ * @param[in] this_instance    The subject block_volume
+ * @param[in] block_store      The associated block_store
+ * @param[in] partition_guid   The partition GUID
+ * @param[out] volume	       The base volume
+ *
+ * @return 0 on success
+ */
+int block_volume_init(
+	struct block_volume *this_instance,
+	struct block_store *block_store,
+	const struct uuid_octets *partition_guid,
+	struct volume **volume);
+
+/**
+ * @brief  De-initialize an block_volume instance
+ *
+ * @param[in] this_instance    The subject block_volume
+ */
+void block_volume_deinit(
+	struct block_volume *this_instance);
+
+/**
+ * @brief  Set the partition GUID
+ *
+ * Modifies the partition GUID. This will be used to identify the target
+ * storage partition on a subsequent call to io_dev_open.
+ *
+ * @param[in] this_instance    The subject block_volume
+ * @param[in] partition_guid   The partition GUID
+ */
+void block_volume_set_partition_guid(
+	struct block_volume *this_instance,
+	const struct uuid_octets *partition_guid);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* MEDIA_BLOCK_VOLUME_H */
diff --git a/components/media/volume/block_io_dev/component.cmake b/components/media/volume/block_volume/component.cmake
similarity index 89%
copy from components/media/volume/block_io_dev/component.cmake
copy to components/media/volume/block_volume/component.cmake
index cba61fc..cfd2c40 100644
--- a/components/media/volume/block_io_dev/component.cmake
+++ b/components/media/volume/block_volume/component.cmake
@@ -9,5 +9,5 @@
 endif()
 
 target_sources(${TGT} PRIVATE
-	"${CMAKE_CURRENT_LIST_DIR}/block_io_dev.c"
+	"${CMAKE_CURRENT_LIST_DIR}/block_volume.c"
 )
\ No newline at end of file
diff --git a/components/media/volume/block_volume/test/block_volume_tests.cpp b/components/media/volume/block_volume/test/block_volume_tests.cpp
new file mode 100644
index 0000000..89d1726
--- /dev/null
+++ b/components/media/volume/block_volume/test/block_volume_tests.cpp
@@ -0,0 +1,335 @@
+/*
+ * Copyright (c) 2022-2023, Arm Limited and Contributors. All rights reserved.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+
+#include <string>
+#include <cstring>
+#include <common/uuid/uuid.h>
+#include <service/block_storage/block_store/device/ram/ram_block_store.h>
+#include <media/volume/block_volume/block_volume.h>
+#include <media/volume/index/volume_index.h>
+#include <CppUTest/TestHarness.h>
+
+TEST_GROUP(BlockVolumeTests)
+{
+	void setup()
+	{
+		uuid_guid_octets_from_canonical(&m_partition_guid,
+			"6152f22b-8128-4c1f-981f-3bd279519907");
+
+		m_block_store = ram_block_store_init(&m_ram_block_store,
+			&m_partition_guid, NUM_BLOCKS, BLOCK_SIZE);
+
+		CHECK_TRUE(m_block_store);
+
+		m_volume = NULL;
+
+		int result = block_volume_init(&m_block_volume,
+			m_block_store, &m_partition_guid,
+			&m_volume);
+
+		LONGS_EQUAL(0, result);
+		CHECK_TRUE(m_volume);
+
+		volume_index_init();
+		volume_index_add(TEST_VOLUME_ID, m_volume);
+	}
+
+	void teardown()
+	{
+		block_volume_deinit(&m_block_volume);
+		ram_block_store_deinit(&m_ram_block_store);
+		volume_index_clear();
+	}
+
+	static const unsigned int TEST_VOLUME_ID = 5;
+	static const size_t NUM_BLOCKS = 100;
+	static const size_t BLOCK_SIZE = 512;
+
+	struct uuid_octets m_partition_guid;
+	struct block_store *m_block_store;
+	struct ram_block_store m_ram_block_store;
+	struct block_volume m_block_volume;
+	struct volume *m_volume;
+};
+
+
+TEST(BlockVolumeTests, openClose)
+{
+	/* Check the open flow used by tf-a components */
+	uintptr_t dev_handle = 0;
+	uintptr_t io_spec = 0;
+	uintptr_t file_handle = 0;
+	int result;
+
+	result = plat_get_image_source(TEST_VOLUME_ID, &dev_handle, &io_spec);
+	LONGS_EQUAL(0, result);
+	CHECK_TRUE(dev_handle);
+
+	result = io_open(dev_handle, io_spec, &file_handle);
+	LONGS_EQUAL(0, result);
+	CHECK_TRUE(file_handle);
+
+	io_close(file_handle);
+}
+
+TEST(BlockVolumeTests, readAndWrite)
+{
+	int result = volume_open(m_volume);
+	LONGS_EQUAL(0, result);
+
+	std::string message("Oh what a beautiful mornin'");
+
+	/* Ensure writes cross a block boundary */
+	size_t num_iterations = BLOCK_SIZE / message.size() + 2;
+
+	/* Write message a few times. Expect file pointer to advance on each write */
+	for (size_t i = 0; i < num_iterations; ++i) {
+
+		size_t len_written = 0;
+
+		result = volume_write(m_volume,
+			(const uintptr_t)message.c_str(), message.size(),
+			&len_written);
+
+		LONGS_EQUAL(0, result);
+		UNSIGNED_LONGS_EQUAL(message.size(), len_written);
+	}
+
+	result = volume_seek(m_volume, IO_SEEK_SET, 0);
+	LONGS_EQUAL(0, result);
+
+	/* Expect to read back the same data */
+	uint8_t read_buf[message.size()];
+
+	for (size_t i = 0; i < num_iterations; ++i) {
+
+		size_t len_read = 0;
+
+		memset(read_buf, 0, sizeof(read_buf));
+
+		result = volume_read(m_volume,
+			(const uintptr_t)read_buf, sizeof(read_buf),
+			&len_read);
+
+		LONGS_EQUAL(0, result);
+		UNSIGNED_LONGS_EQUAL(message.size(), len_read);
+		MEMCMP_EQUAL(message.c_str(), read_buf, message.size());
+	}
+
+	result = volume_close(m_volume);
+	LONGS_EQUAL(0, result);
+}
+
+TEST(BlockVolumeTests, seekAccess)
+{
+	size_t len = 0;
+
+	int result = volume_open(m_volume);
+	LONGS_EQUAL(0, result);
+
+	std::string message("Knees up Mother Brown");
+
+	/* Initially seek to an arbitrary position around the middle of the volume */
+	size_t start_pos = (NUM_BLOCKS * BLOCK_SIZE) / 2 + 27;
+
+	/* Seek and write a few times */
+	result = volume_seek(m_volume, IO_SEEK_SET, start_pos);
+	LONGS_EQUAL(0, result);
+
+	result = volume_write(m_volume, (const uintptr_t)message.c_str(), message.size(), &len);
+	LONGS_EQUAL(0, result);
+	UNSIGNED_LONGS_EQUAL(message.size(), len);
+
+	/* Using IO_SEEK_SET, seek forward, skipping over the written message */
+	result = volume_seek(m_volume, IO_SEEK_SET, start_pos + 110);
+	LONGS_EQUAL(0, result);
+
+	result = volume_write(m_volume, (const uintptr_t)message.c_str(), message.size(), &len);
+	LONGS_EQUAL(0, result);
+	UNSIGNED_LONGS_EQUAL(message.size(), len);
+
+	/* Using IO_SEEK_CUR, seek forward again, far enough to skip over the message */
+	result = volume_seek(m_volume, IO_SEEK_CUR, 715);
+	LONGS_EQUAL(0, result);
+
+	result = volume_write(m_volume, (const uintptr_t)message.c_str(), message.size(), &len);
+	LONGS_EQUAL(0, result);
+	UNSIGNED_LONGS_EQUAL(message.size(), len);
+
+	/* Perform the same sequence of seeks and expect to read back intact copies of the message */
+	uint8_t read_buf[message.size()];
+
+	result = volume_seek(m_volume, IO_SEEK_SET, start_pos);
+	LONGS_EQUAL(0, result);
+
+	result = volume_read(m_volume, (uintptr_t)read_buf, sizeof(read_buf), &len);
+	LONGS_EQUAL(0, result);
+	UNSIGNED_LONGS_EQUAL(message.size(), len);
+	MEMCMP_EQUAL(message.c_str(), read_buf, message.size());
+
+	result = volume_seek(m_volume, IO_SEEK_SET, start_pos + 110);
+	LONGS_EQUAL(0, result);
+
+	result = volume_read(m_volume, (uintptr_t)read_buf, sizeof(read_buf), &len);
+	LONGS_EQUAL(0, result);
+	UNSIGNED_LONGS_EQUAL(message.size(), len);
+	MEMCMP_EQUAL(message.c_str(), read_buf, message.size());
+
+	result = volume_seek(m_volume, IO_SEEK_CUR, 715);
+	LONGS_EQUAL(0, result);
+
+	result = volume_read(m_volume, (uintptr_t)read_buf, sizeof(read_buf), &len);
+	LONGS_EQUAL(0, result);
+	UNSIGNED_LONGS_EQUAL(message.size(), len);
+	MEMCMP_EQUAL(message.c_str(), read_buf, message.size());
+
+	result = volume_close(m_volume);
+	LONGS_EQUAL(0, result);
+}
+
+TEST(BlockVolumeTests, multipleImageInstall)
+{
+	/* Test that a sequence of image install operations can be applied
+	 * to the same volume. Prior to performing streamed write operations to
+	 * install an image, the previous content must be erased, if the backend
+	 * storage requires this e.g. for NOR flash. */
+
+	struct volume *volume = NULL;
+	int result;
+
+	result = volume_index_find(TEST_VOLUME_ID, &volume);
+	LONGS_EQUAL(0, result);
+	CHECK_TRUE(volume);
+
+	for (size_t i = 0; i < 3; ++i) {
+
+		size_t len = 0;
+
+		/* Each iteration represents an update installation where arbitrary sized
+		 * chunks are written to the storage volume. */
+		result = volume_open(volume);
+		LONGS_EQUAL(0, result);
+
+		/* Writes will only succeed if the written blocks have already been erased.
+		 * By repeatedly writing to the same blocks, this test verifies that the erase
+		 * must be working correctly.
+		 */
+		result = volume_erase(volume);
+		LONGS_EQUAL(0, result);
+
+		std::string chunk1("The first chunk of the update image");
+		result = volume_write(volume, (const uintptr_t)chunk1.c_str(), chunk1.size(), &len);
+		LONGS_EQUAL(0, result);
+		UNSIGNED_LONGS_EQUAL(chunk1.size(), len);
+
+		std::string chunk2("And the second");
+		result = volume_write(volume, (const uintptr_t)chunk2.c_str(), chunk2.size(), &len);
+		LONGS_EQUAL(0, result);
+		UNSIGNED_LONGS_EQUAL(chunk2.size(), len);
+
+		std::string chunk3("The third chunck is soooooooooooooooooooooooooooo much longer");
+		result = volume_write(volume, (const uintptr_t)chunk3.c_str(), chunk3.size(), &len);
+		LONGS_EQUAL(0, result);
+		UNSIGNED_LONGS_EQUAL(chunk3.size(), len);
+
+		/* Try reading it all back */
+		std::string expected_data = chunk1 + chunk2 + chunk3;
+		uint8_t read_buf[expected_data.size()];
+
+		result = volume_seek(volume, IO_SEEK_SET, 0);
+		LONGS_EQUAL(0, result);
+
+		result = volume_read(volume, (uintptr_t)read_buf, sizeof(read_buf), &len);
+		LONGS_EQUAL(0, result);
+		UNSIGNED_LONGS_EQUAL(expected_data.size(), len);
+		MEMCMP_EQUAL(expected_data.c_str(), read_buf, expected_data.size());
+
+		result = volume_close(volume);
+		LONGS_EQUAL(0, result);
+	}
+}
+
+TEST(BlockVolumeTests, oversizeWrite)
+{
+	int result = volume_open(m_volume);
+	LONGS_EQUAL(0, result);
+
+	size_t vol_size;
+	result = volume_size(m_volume, &vol_size);
+	LONGS_EQUAL(0, result);
+	CHECK_TRUE(vol_size > 0);
+
+	std::string message("Message to be written many many times");
+
+	/* Expect to be able write the whole message lots of times without error */
+	size_t num_whole_messages = vol_size / message.size();
+	size_t space_remaining = vol_size % message.size();
+
+	/* Expect there to be remaining bytes free at the end of the volume*/
+	CHECK_TRUE(space_remaining > 0);
+
+	for (size_t i = 0; i < num_whole_messages; ++i) {
+
+		size_t len_written = 0;
+
+		result = volume_write(m_volume,
+			(const uintptr_t)message.c_str(), message.size(),
+			&len_written);
+
+		LONGS_EQUAL(0, result);
+		UNSIGNED_LONGS_EQUAL(message.size(), len_written);
+	}
+
+	/* Writing the message one more time should exceed the volume size */
+	size_t len_written = 0;
+
+	result = volume_write(m_volume,
+		(const uintptr_t)message.c_str(), message.size(),
+		&len_written);
+
+	LONGS_EQUAL(0, result);
+
+	/* Expect the number of bytes written gets truncated to the size limit of the volume */
+	UNSIGNED_LONGS_EQUAL(space_remaining, len_written);
+
+	result = volume_seek(m_volume, IO_SEEK_SET, 0);
+	LONGS_EQUAL(0, result);
+
+	/* Expect to read back the same number of whole messages */
+	uint8_t read_buf[message.size()];
+
+	for (size_t i = 0; i < num_whole_messages; ++i) {
+
+		size_t len_read = 0;
+
+		memset(read_buf, 0, sizeof(read_buf));
+
+		result = volume_read(m_volume,
+			(const uintptr_t)read_buf, sizeof(read_buf),
+			&len_read);
+
+		LONGS_EQUAL(0, result);
+		UNSIGNED_LONGS_EQUAL(message.size(), len_read);
+		MEMCMP_EQUAL(message.c_str(), read_buf, message.size());
+	}
+
+	/* Expect final read to be truncated to the end of the volume */
+	size_t len_read = 0;
+
+	memset(read_buf, 0, sizeof(read_buf));
+
+	result = volume_read(m_volume,
+		(const uintptr_t)read_buf, sizeof(read_buf),
+		&len_read);
+
+	LONGS_EQUAL(0, result);
+	UNSIGNED_LONGS_EQUAL(space_remaining, len_read);
+	MEMCMP_EQUAL(message.c_str(), read_buf, space_remaining);
+
+	result = volume_close(m_volume);
+	LONGS_EQUAL(0, result);
+}
diff --git a/components/media/volume/block_io_dev/test/component.cmake b/components/media/volume/block_volume/test/component.cmake
similarity index 88%
rename from components/media/volume/block_io_dev/test/component.cmake
rename to components/media/volume/block_volume/test/component.cmake
index 7be3a61..e28856b 100644
--- a/components/media/volume/block_io_dev/test/component.cmake
+++ b/components/media/volume/block_volume/test/component.cmake
@@ -9,5 +9,5 @@
 endif()
 
 target_sources(${TGT} PRIVATE
-	"${CMAKE_CURRENT_LIST_DIR}/block_io_dev_tests.cpp"
+	"${CMAKE_CURRENT_LIST_DIR}/block_volume_tests.cpp"
 	)
diff --git a/components/media/volume/block_io_dev/component.cmake b/components/media/volume/component.cmake
similarity index 89%
rename from components/media/volume/block_io_dev/component.cmake
rename to components/media/volume/component.cmake
index cba61fc..9ef1737 100644
--- a/components/media/volume/block_io_dev/component.cmake
+++ b/components/media/volume/component.cmake
@@ -9,5 +9,5 @@
 endif()
 
 target_sources(${TGT} PRIVATE
-	"${CMAKE_CURRENT_LIST_DIR}/block_io_dev.c"
+	"${CMAKE_CURRENT_LIST_DIR}/volume.c"
 )
\ No newline at end of file
diff --git a/components/media/volume/index/volume_index.c b/components/media/volume/index/volume_index.c
index 7f68616..0c6aa99 100644
--- a/components/media/volume/index/volume_index.c
+++ b/components/media/volume/index/volume_index.c
@@ -11,20 +11,18 @@
 #include "volume_index.h"
 
 #ifndef VOLUME_INDEX_MAX_ENTRIES
-#define VOLUME_INDEX_MAX_ENTRIES		(4)
+#define VOLUME_INDEX_MAX_ENTRIES		(8)
 #endif
 
 /**
  * Singleton index of volume IDs to IO devices.
  */
-static struct
-{
+static struct {
+
 	size_t size;
-	struct
-	{
+	struct {
 		unsigned int volume_id;
-		uintptr_t dev_handle;
-		uintptr_t volume_spec;
+		struct volume *volume;
 	} entries[VOLUME_INDEX_MAX_ENTRIES];
 
 } volume_index;
@@ -34,7 +32,7 @@
  *
  * @param[in]  volume_id 	Identifies the image
  * @param[out] dev_handle 	Handle for IO operations
- * @param[out] volume_spec	Opaque configuration data
+ * @param[out] io_spec	Opaque configuration data
  *
  * This function realizes the interface expected by tf-a components to
  * provide a concrete IO device for the specified volume ID. When used in
@@ -44,20 +42,19 @@
 int plat_get_image_source(
 	unsigned int volume_id,
 	uintptr_t *dev_handle,
-	uintptr_t *volume_spec)
+	uintptr_t *io_spec)
 {
-	int result = -1;
+	struct volume *volume = NULL;
+	int result = volume_index_find(volume_id, &volume);
 
-	for (size_t i = 0; i < volume_index.size; i++) {
+	if (result == 0) {
 
-		if (volume_index.entries[i].volume_id == volume_id) {
+		if (volume) {
 
-			*dev_handle = volume_index.entries[i].dev_handle;
-			*volume_spec = volume_index.entries[i].volume_spec;
-
-			result = 0;
-			break;
-		}
+			*dev_handle = volume->dev_handle;
+			*io_spec = volume->io_spec;
+		} else
+			result = -1;
 	}
 
 	return result;
@@ -75,22 +72,48 @@
 
 int volume_index_add(
 	unsigned int volume_id,
-	uintptr_t dev_handle,
-	uintptr_t volume_spec)
+	struct volume *volume)
 {
 	int result = -1;
 
-	if (volume_index.size < VOLUME_INDEX_MAX_ENTRIES)
-	{
+	if (volume_index.size < VOLUME_INDEX_MAX_ENTRIES) {
 		size_t i = volume_index.size;
 
 		++volume_index.size;
 		volume_index.entries[i].volume_id = volume_id;
-		volume_index.entries[i].dev_handle = dev_handle;
-		volume_index.entries[i].volume_spec = volume_spec;
+		volume_index.entries[i].volume = volume;
 
 		result = 0;
 	}
 
 	return result;
 }
+
+int volume_index_find(
+	unsigned int volume_id,
+	struct volume **volume)
+{
+	int result = -1;
+
+	for (size_t i = 0; i < volume_index.size; i++) {
+
+		if (volume_index.entries[i].volume_id == volume_id) {
+
+			*volume = volume_index.entries[i].volume;
+			result = 0;
+			break;
+		}
+	}
+
+	return result;
+}
+
+struct volume *volume_index_get(unsigned int index)
+{
+	struct volume *volume = NULL;
+
+	if (index < volume_index.size)
+		volume = volume_index.entries[index].volume;
+
+	return volume;
+}
diff --git a/components/media/volume/index/volume_index.h b/components/media/volume/index/volume_index.h
index 97bb3b8..612d0e9 100644
--- a/components/media/volume/index/volume_index.h
+++ b/components/media/volume/index/volume_index.h
@@ -8,6 +8,7 @@
 #define MEDIA_VOLUME_INDEX_H
 
 #include <stdint.h>
+#include <media/volume/volume.h>
 
 #ifdef __cplusplus
 extern "C" {
@@ -25,17 +26,18 @@
  * Volume IDs only need to be unique within a deployment. For convenience,
  * here are some common volume IDs that may be useful.
  */
-#define VOLUME_ID_COMMON_BASE        (0x10000)
+#define VOLUME_ID_COMMON_BASE        (0x10000000)
 #define VOLUME_ID_SECURE_FLASH       (VOLUME_ID_COMMON_BASE + 0)
 
 /**
  * @brief  Initialize the volume index
  *
  * The volume_index is a singleton that holds the mapping of volume IDs
- * to concrete IO devices that can be used to access the volume. The
- * mappings are setup during deployment configuration to meet the IO needs
- * of the deployment. The volume_index realizes the tf-a function
- * plat_get_image_source() to make the mappings available to tf-a components.
+ * to concrete volume objects that associate a storage volume with an
+ * IO device that may be used to access storage. The mappings are setup
+ * during deployment configuration to meet the IO needs of the deployment.
+ * The volume_index realizes the tf-a function plat_get_image_source() to
+ * make the mappings available to tf-a components.
  */
 void volume_index_init(void);
 
@@ -50,15 +52,35 @@
  * @brief  Add an entry to the volume index
  *
  * @param[in] volume_id   Volume identifier
- * @param[in] dev_handle  Device handle to use
- * @param[in] volume_spec  Additional information about the volume
+ * @param[in] volume      The volume that extends the base io_dev
  *
  * @return 0 if successful
  */
 int volume_index_add(
 	unsigned int volume_id,
-	uintptr_t dev_handle,
-	uintptr_t volume_spec);
+	struct volume *volume);
+
+/**
+ * @brief  Find an added volume by volume index
+ *
+ * @param[in]  volume_id   Volume identifier
+ * @param[out] volume      The volume that extends the base io_dev
+ *
+ * @return 0 if found
+ */
+int volume_index_find(
+	unsigned int volume_id,
+	struct volume **volume);
+
+/**
+ * @brief  Iterator function
+ *
+ * @param[in]  index  0..n
+ *
+ * @return Pointer to a concrete volume or NULL if iterated beyond final entry
+ */
+struct volume *volume_index_get(unsigned int index);
+
 
 #ifdef __cplusplus
 }
diff --git a/components/media/volume/volume.c b/components/media/volume/volume.c
new file mode 100644
index 0000000..b043376
--- /dev/null
+++ b/components/media/volume/volume.c
@@ -0,0 +1,95 @@
+/*
+ * Copyright (c) 2022, Arm Limited and Contributors. All rights reserved.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+#include <errno.h>
+#include <stddef.h>
+#include "volume.h"
+
+void volume_init(
+	struct volume *this_volume,
+	const io_dev_funcs_t *io_dev_funcs,
+	uintptr_t concrete_volume)
+{
+	this_volume->dev_info.funcs = io_dev_funcs;
+	this_volume->dev_info.info = concrete_volume;
+
+	this_volume->dev_handle = (uintptr_t)&this_volume->dev_info;
+	this_volume->io_spec = concrete_volume;
+
+	this_volume->io_handle = 0;
+
+	/* Optional functions that a concrete volume may provide */
+	this_volume->erase = NULL;
+	this_volume->get_storage_ids = NULL;
+}
+
+int volume_open(
+	struct volume *this_volume)
+{
+	return io_open(
+		this_volume->dev_handle,
+		this_volume->io_spec,
+		&this_volume->io_handle);
+}
+
+int volume_close(
+	struct volume *this_volume)
+{
+	return io_close(this_volume->io_handle);
+}
+
+int volume_seek(
+	struct volume *this_volume,
+	io_seek_mode_t mode,
+	signed long long offset)
+{
+	return io_seek(this_volume->io_handle, mode, offset);
+}
+
+int volume_size(
+	struct volume *this_volume,
+	size_t *size)
+{
+	return io_size(this_volume->io_handle, size);
+}
+
+int volume_read(
+	struct volume *this_volume,
+	uintptr_t buffer,
+	size_t length,
+	size_t *length_read)
+{
+	return io_read(this_volume->io_handle, buffer, length, length_read);
+}
+
+int volume_write(
+	struct volume *this_volume,
+	const uintptr_t buffer,
+	size_t length,
+	size_t *length_written)
+{
+	return io_write(this_volume->io_handle, buffer, length, length_written);
+}
+
+int volume_erase(
+	struct volume *this_volume)
+{
+	return (this_volume->erase) ?
+		this_volume->erase(this_volume->dev_info.info) : 0;
+}
+
+int volume_get_storage_ids(
+	struct volume *this_volume,
+	struct uuid_octets *partition_guid,
+	struct uuid_octets *parent_guid)
+{
+	if (this_volume->get_storage_ids)
+		return this_volume->get_storage_ids(
+			this_volume->dev_info.info,
+			partition_guid, parent_guid);
+
+	return -EIO;
+}
diff --git a/components/media/volume/volume.h b/components/media/volume/volume.h
new file mode 100644
index 0000000..3395e5b
--- /dev/null
+++ b/components/media/volume/volume.h
@@ -0,0 +1,182 @@
+/*
+ * Copyright (c) 2022, Arm Limited and Contributors. All rights reserved.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+#ifndef MEDIA_VOLUME_H
+#define MEDIA_VOLUME_H
+
+#include <stddef.h>
+#include <common/uuid/uuid.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * Export tf-a version with C++ linkage support.
+ */
+#include <drivers/io/io_driver.h>
+#include <drivers/io/io_storage.h>
+
+/**
+ * A volume can be used to access storage as a single seekable volume
+ * that may be accessed using file io type operations. The base volume
+ * extends the TF-A io_dev to add an erase operation to enable the entire
+ * volume to be erased prior to performing write operations. This is useful
+ * when doing things like installing firmware into a raw disk partition.
+ * Alternative concrete volume implementations are possible to suite different
+ * classes of storage. If a concrete volume does not support erase, the
+ * erase function pointer may be set to NULL;
+ */
+struct volume {
+
+	/* IO device handle for volume access */
+	uintptr_t dev_handle;
+
+	/* Opaque IO spec */
+	uintptr_t io_spec;
+
+	/* Base IO device */
+	io_dev_info_t dev_info;
+
+	/* Handle for IO operations */
+	uintptr_t io_handle;
+
+	/* Optional function to erase the volume */
+	int (*erase)(uintptr_t context);
+
+	/* Optional function to get storage IDs for the volume */
+	int (*get_storage_ids)(uintptr_t context,
+		struct uuid_octets *partition_guid,
+		struct uuid_octets *parent_guid);
+};
+
+/**
+ * @brief  Initialize the base volume structure
+ *
+ * Called by a concrete volume to initialize the base volume and io_dev.
+ *
+ * @param[in] this_volume    The subject volume
+ * @param[in] io_dev_funcs   io_dev function struct for concrete handlers
+ * @param[in] concrete_volume Pointer to the concrete volume instance
+ */
+void volume_init(
+	struct volume *this_volume,
+	const io_dev_funcs_t *io_dev_funcs,
+	uintptr_t concrete_volume);
+
+/**
+ * @brief  Open the volume for IO operations
+ *
+ * @param[in] this_volume    The subject volume
+ *
+ * @return 0 on success
+ */
+int volume_open(
+	struct volume *this_volume);
+
+/**
+ * @brief  Close the volume when done with IO operations
+ *
+ * @param[in] this_volume    The subject volume
+ *
+ * @return 0 on success
+ */
+int volume_close(
+	struct volume *this_volume);
+
+/**
+ * @brief  Seek to the specified position
+ *
+ * @param[in] this_volume    The subject volume
+ * @param[in] mode   See io_storage.h for options
+ * @param[in] offset Seek offset
+ *
+ * @return 0 on success
+ */
+int volume_seek(
+	struct volume *this_volume,
+	io_seek_mode_t mode,
+	signed long long offset);
+
+/**
+ * @brief  Get the size of the volume
+ *
+ * @param[in] this_volume    The subject volume
+ * @param[out] size   Size in bytes of the volume
+ *
+ * @return 0 on success
+ */
+int volume_size(
+	struct volume *this_volume,
+	size_t *size);
+
+/**
+ * @brief  Read from the volume
+ *
+ * Reads from the current seek position.
+ *
+ * @param[in] this_volume    The subject volume
+ * @param[in] buffer   Buffer to put read data
+ * @param[in] length   Requested read length
+ * @param[out] length_read Actual length read
+ *
+ * @return 0 on success
+ */
+int volume_read(
+	struct volume *this_volume,
+	uintptr_t buffer,
+	size_t length,
+	size_t *length_read);
+
+/**
+ * @brief  Write the volume
+ *
+ * Write from the current seek position.
+ *
+ * @param[in] this_volume    The subject volume
+ * @param[in] buffer   Buffer containing data to write
+ * @param[in] length   Requested write length
+ * @param[out] length_read Actual write read
+ *
+ * @return 0 on success
+ */
+int volume_write(
+	struct volume *this_volume,
+	const uintptr_t buffer,
+	size_t length,
+	size_t *length_written);
+
+/**
+ * @brief  Erase the entire volume
+ *
+ * @param[in] this_volume    The subject volume
+ *
+ * @return 0 on success
+ */
+int volume_erase(
+	struct volume *this_volume);
+
+/**
+ * @brief  Get GUIDs to identify the storage associated with the volume
+ *
+ * If supported by a concrete volume
+ *
+ * @param[in] this_volume    The subject volume
+ * @param[out] partition_guid GUID for the logical partition
+ * @param[out] parent_guid GUID for a parent device e.g. a disk GUID
+ *
+ * @return 0 on success, ENOSYS if not supported
+ */
+int volume_get_storage_ids(
+	struct volume *this_volume,
+	struct uuid_octets *partition_guid,
+	struct uuid_octets *parent_guid);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* MEDIA_VOLUME_H */
diff --git a/components/service/block_storage/factory/ref_ram_gpt/block_store_factory.c b/components/service/block_storage/factory/ref_ram_gpt/block_store_factory.c
index dbea84a..d72e6aa 100644
--- a/components/service/block_storage/factory/ref_ram_gpt/block_store_factory.c
+++ b/components/service/block_storage/factory/ref_ram_gpt/block_store_factory.c
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2022, Arm Limited. All rights reserved.
+ * Copyright (c) 2022-2023, Arm Limited. All rights reserved.
  *
  * SPDX-License-Identifier: BSD-3-Clause
  *
@@ -15,7 +15,7 @@
 #include <service/block_storage/config/ref/ref_partition_configurator.h>
 #include <service/block_storage/config/gpt/gpt_partition_configurator.h>
 #include <media/volume/index/volume_index.h>
-#include <media/volume/block_io_dev/block_io_dev.h>
+#include <media/volume/block_volume/block_volume.h>
 #include <media/disk/disk_images/ref_partition.h>
 #include <media/disk/formatter/disk_formatter.h>
 #include "block_store_factory.h"
@@ -24,7 +24,7 @@
 {
 	struct ram_block_store ram_block_store;
 	struct partitioned_block_store partitioned_block_store;
-	struct block_io_dev volume_io;
+	struct block_volume volume;
 };
 
 static void tear_down_assembly(struct block_store_assembly *assembly)
@@ -33,7 +33,7 @@
 
 	partitioned_block_store_deinit(&assembly->partitioned_block_store);
 	ram_block_store_deinit(&assembly->ram_block_store);
-	block_io_dev_deinit(&assembly->volume_io);
+	block_volume_deinit(&assembly->volume);
 
 	free(assembly);
 }
@@ -55,7 +55,8 @@
 		assert(!(ref_partition_data_length % REF_PARTITION_BLOCK_SIZE));
 
 		/* Initialise a ram_block_store to mimic the secure flash used
-		 * to provide underlying storage. */
+		 * to provide underlying storage.
+		 */
 		struct block_store *secure_flash = ram_block_store_init(
 			&assembly->ram_block_store,
 			&disk_guid,
@@ -64,20 +65,19 @@
 
 		if (secure_flash) {
 
-			/* Secure flash successfully initialized so create an io_dev to
-			 * enable it to be accessed as a storage volume. The io_dev is
-			 * used to initialize the flash with the reference disk image. */
-			uintptr_t dev_handle = 0;
-			uintptr_t volume_spec = 0;
+			/* Secure flash successfully initialized so create a block_volume to
+			 * enable it to be accessed as a storage volume. The created io_dev is
+			 * used to initialize the flash with the reference disk image.
+			 */
+			struct volume *volume = NULL;
 
-			if (!block_io_dev_init(&assembly->volume_io,
-					secure_flash, &disk_guid,
-					&dev_handle, &volume_spec) &&
+			if (!block_volume_init(&assembly->volume,
+					secure_flash, &disk_guid, &volume) &&
 				!disk_formatter_clone(
-					dev_handle, volume_spec,
+					volume->dev_handle, volume->io_spec,
 					ref_partition_data, ref_partition_data_length)) {
 
-				volume_index_add(VOLUME_ID_SECURE_FLASH, dev_handle, volume_spec);
+				volume_index_add(VOLUME_ID_SECURE_FLASH, volume);
 
 				/* Stack a partitioned_block_store over the back store */
 				product = partitioned_block_store_init(
@@ -89,7 +89,8 @@
 				if (product) {
 
 					/* Successfully created the block store stack so configure the
-					 * partitions if there are any described in the GPT. */
+					 * partitions if there are any described in the GPT.
+					 */
 					gpt_partition_configure(
 						&assembly->partitioned_block_store,
 						VOLUME_ID_SECURE_FLASH);
diff --git a/components/service/block_storage/factory/semihosting/block_store_factory.c b/components/service/block_storage/factory/semihosting/block_store_factory.c
index 17296d5..8e58e36 100644
--- a/components/service/block_storage/factory/semihosting/block_store_factory.c
+++ b/components/service/block_storage/factory/semihosting/block_store_factory.c
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2022, Arm Limited. All rights reserved.
+ * Copyright (c) 2022-2023, Arm Limited. All rights reserved.
  *
  * SPDX-License-Identifier: BSD-3-Clause
  *
@@ -16,7 +16,7 @@
 #include "service/block_storage/block_store/partitioned/partitioned_block_store.h"
 #include "service/block_storage/config/gpt/gpt_partition_configurator.h"
 #include "media/volume/index/volume_index.h"
-#include "media/volume/block_io_dev/block_io_dev.h"
+#include "media/volume/block_volume/block_volume.h"
 
 /* Most common block size for UEFI volumes */
 #define SEMIHOSTING_BLOCK_SIZE		(512)
@@ -25,7 +25,7 @@
 {
 	struct semihosting_block_store semihosting_block_store;
 	struct partitioned_block_store partitioned_block_store;
-	struct block_io_dev volume_io;
+	struct block_volume volume;
 };
 
 static void tear_down_assembly(struct block_store_assembly *assembly)
@@ -34,7 +34,7 @@
 
 	partitioned_block_store_deinit(&assembly->partitioned_block_store);
 	semihosting_block_store_deinit(&assembly->semihosting_block_store);
-	block_io_dev_deinit(&assembly->volume_io);
+	block_volume_deinit(&assembly->volume);
 
 	free(assembly);
 }
@@ -60,21 +60,19 @@
 
 		if (secure_flash) {
 
-			/* Secure flash successfully initialized so create an io_dev to
-			 * enable it to be accessed as a storage volume.
+			/* Secure flash successfully initialized so create a block_volume
+			 * associated with secure flash block store to enable it to be
+			 * accessed as a storage volume.
 			 */
-			uintptr_t dev_handle = 0;
-			uintptr_t volume_spec = 0;
+			struct volume *volume = NULL;
 
-			int result = block_io_dev_init(&assembly->volume_io,
+			int result = block_volume_init(&assembly->volume,
 				secure_flash, &disk_guid,
-				&dev_handle, &volume_spec);
+				&volume);
 
 			if (result == 0) {
 
-				int status = volume_index_add(VOLUME_ID_SECURE_FLASH,
-					dev_handle, volume_spec);
-				assert(status == 0);
+				volume_index_add(VOLUME_ID_SECURE_FLASH, volume);
 
 				/* Stack a partitioned_block_store over the back store */
 				product = partitioned_block_store_init(