Skip to main content
Version: 3.0.0-rc.2

Adapt a custom device or firmware reader

Every Hadris filesystem driver reads a hadris-storage block device, and the CPIO reader and writer read hadris-io streams. Neither needs std::io. Synchronous streams can implement the hadris-io traits and use sync::StreamDevice. Asynchronous block devices implement the common poll contract directly, or use async_::StreamDevice with a poll-native async_::Stream. BlockingStream explicitly bridges synchronous streams and blocks during each poll. Older async-trait stream adapters remain under legacy_async and legacy_local; embedded-io async streams continue to work with CPIO's existing stream contract.

For a filesystem, implement a block device directly when the hardware addresses whole blocks.

Implement the stream traits​

[dependencies]
hadris-io = { version = "3.0.0-rc.2", default-features = false, features = ["sync"] }

hadris_io::sync::Read, Write and Seek report the implementor's own error through the ErrorType supertrait, which can be any core::error::Error + Send + Sync + 'static. Implement only read, write and flush, and seek; the other methods have defaults.

use core::fmt;

use hadris_io::sync::{Read, Seek};
use hadris_io::{ErrorType, SeekFrom};

#[derive(Debug)]
enum DiskError {
Io,
BadSeek,
}

impl fmt::Display for DiskError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
DiskError::Io => "firmware read failed",
DiskError::BadSeek => "seek outside the device",
})
}
}

impl core::error::Error for DiskError {}

struct FirmwareDisk {
position: u64,
len: u64,
}

impl ErrorType for FirmwareDisk {
type Error = DiskError;
}

impl Read for FirmwareDisk {
fn read(&mut self, buf: &mut [u8]) -> Result<usize, DiskError> {
// Read from the firmware or device protocol into `buf`, then advance
// `self.position`.
let _ = buf;
Err(DiskError::Io)
}
}

impl Seek for FirmwareDisk {
fn seek(&mut self, pos: SeekFrom) -> Result<u64, DiskError> {
match pos.resolve(self.position, self.len) {
Some(position) if position <= self.len => {
self.position = position;
Ok(position)
}
_ => Err(DiskError::BadSeek),
}
}
}

let disk = FirmwareDisk { position: 0, len: 64 * 1024 * 1024 };
// Pass `disk` to a stream consumer, or to `StreamDevice::new` for a filesystem.

The device's error reaches the caller unchanged, inside hadris_io::ExactError or the format crate's error, with no allocation. SeekFrom is #[non_exhaustive], so resolve it with SeekFrom::resolve rather than matching it.

&mut FirmwareDisk implements the same traits, so a caller can pass &mut disk to a format crate and keep ownership of the device.

Wrap an embedded-io device​

A device that already implements the embedded-io traits is wrapped in FromEmbedded, which needs the embedded-io feature. Its error passes through unchanged.

[dependencies]
embedded-io = "0.7"
hadris-io = { version = "3.0.0-rc.2", default-features = false, features = ["sync", "embedded-io"] }
use embedded_io::{ErrorKind, ErrorType, Read, Seek, SeekFrom};
use hadris_io::FromEmbedded;

struct FirmwareDisk {
// Firmware protocol handle and current position.
}

impl ErrorType for FirmwareDisk {
type Error = ErrorKind;
}

impl Read for FirmwareDisk {
fn read(&mut self, buf: &mut [u8]) -> Result<usize, Self::Error> {
// Read from the firmware or device protocol into `buf`.
todo!()
}
}

impl Seek for FirmwareDisk {
fn seek(&mut self, position: SeekFrom) -> Result<u64, Self::Error> {
// Validate the requested position and update the device cursor.
todo!()
}
}

let disk = FromEmbedded::new(FirmwareDisk { /* ... */ });
// Pass `disk` to a stream consumer, or to `StreamDevice::new` for a filesystem.

The device error type can be any embedded_io::Error that is Send + Sync + 'static; ErrorKind is used here for brevity. With the async feature, FromEmbedded wraps embedded-io-async devices for the async traits in the same way.

Implement a block device for FAT​

hadris::fat::sync::FatFs mounts any hadris::storage::sync::BlockDevice. A device reports its block size and count and reads whole blocks; a read-only device leaves write_blocks to its default, which answers kind ReadOnly. Every method returns hadris::io::Error over the device's own error type, which implements core::error::Error; Error::device wraps a device failure with a static message, and FatFs passes it on unchanged. The device adapter needs no allocator; FatFs itself needs alloc. Allocation-free firmware uses hadris::fat::embedded::sync::Fat instead.

[dependencies]
hadris = { version = "3.0.0-rc.2", default-features = false, features = ["alloc", "sync", "fat"] }
use core::fmt;

use hadris::io::{Error, ErrorType, Location};
use hadris::storage::sync::BlockDevice;
use hadris::storage::{BlockIndex, BlockSize};

#[derive(Debug)]
struct FirmwareError;

impl fmt::Display for FirmwareError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("firmware block read failed")
}
}

impl core::error::Error for FirmwareError {}

struct FirmwareDisk {
blocks: u64,
}

impl ErrorType for FirmwareDisk {
type Error = FirmwareError;
}

impl BlockDevice for FirmwareDisk {
fn block_size(&self) -> BlockSize {
BlockSize::new(512).unwrap()
}

fn block_count(&self) -> u64 {
self.blocks
}

fn read_blocks(&mut self, first: BlockIndex, buf: &mut [u8]) -> Result<(), Error<FirmwareError>> {
// Read `buf.len() / 512` blocks starting at `first` from the device.
let _ = buf;
Err(Error::device(FirmwareError, "reading a block failed")
.with_location(Location::Block(first.get())))
}
}

let disk = FirmwareDisk { blocks: 131_072 };
let volume = hadris::fat::sync::FatFs::mount(disk, hadris::fs::MountOptions::new());

A byte stream implementing the hadris-io traits becomes a block device through hadris::storage::sync::StreamDevice, and hadris::storage::host::FileDevice, Vec<u8> and hadris::storage::MemDevice are block devices already.

Device requirements​

Report the device's real block size and count, read and write whole blocks only, and fail requests past the end rather than wrapping. A device that cannot write leaves writable and write_blocks to their defaults: it is not writable, so drivers mount it read-only, and a write returns kind ReadOnly. A device that can write returns true from writable and implements write_blocks; it may still refuse a write with ReadOnly, for example when its media become write-protected, and drivers then stop writing. A request past the end fails with kind InvalidInput. flush must make earlier writes durable, because sync and fsync rely on it.

Keep the device's block size and the filesystem's logical sector size distinct: the drivers read whole device blocks and take their own sector size from the on-disk metadata.

For memory-backed parsing without std, use hadris_storage::MemDevice over a caller-provided byte slice, or hadris_io::Cursor for a stream.

Hardware alignment and transfer limits​

Hadris may pass a caller's subslice directly to read_blocks or write_blocks. Only the length is block-aligned: the memory address has no additional alignment guarantee. Implementations must accept any address and any whole-block length. Use an appropriately aligned bounce buffer or DMA-accessible buffer when the controller requires one, and split a large request into supported transfers. Reject invalid lengths, out-of-range requests and address overflow before starting hardware I/O.

UEFI Block I/O exposes IoAlign separately from BlockSize; similarly, a DMA controller may require memory from a particular region. Keep those requirements inside the device adapter. The compiled aligned-device example uses a fixed 512-byte buffer aligned to 64 bytes and transfers one block at a time, with no allocation. Run it with:

cargo run -p hadris-storage --no-default-features --features sync --example aligned_device

Replace the example's Controller implementation with the hardware protocol. Each synchronous controller call must complete its transfer before returning. Async poll hooks must stop accessing caller buffers before returning, including Pending. DMA continuing between polls needs owned stable transfer buffers. State must not retain pointers into its own movable fields. Buffer safety must hold even if a future is forgotten and cancellation never runs; stopping hardware only when the future is dropped does not protect caller buffers between polls. Successful block operations transfer the whole requested buffer. A failed write may already have transferred earlier blocks and does not promise rollback.

async_::BlockDevice (also available as local::BlockDevice) allows non-Send devices and futures for single-threaded executors. Its error still implements core::error::Error + Send + Sync + 'static, as it does in the other modes.

Raw NOR/NAND flash needs erase handling and a layer that supplies block overwrite semantics; the embedded-storage NOR contract exposes separate erase and write granularities. Use a flash adapter or translation layer before presenting it as a rewritable BlockDevice.

RC2 asynchronous block devices​

Use hadris_storage::async_::BlockDevice and supply State, poll_read_blocks, optional poll_write_blocks/poll_flush, and cancel. Callers await the provided read/write/flush methods. SendBlockDevice is automatic when the device and state are Send; the same filesystem type works with local devices too. r#async remains a compatibility alias.

Hooks must release access to caller buffers before returning Pending and arrange wakeups when progress becomes possible. cancel must be safe when idle and must not panic, including during unwinding before I/O starts. State must not retain pointers to its own movable fields. Continuing hardware I/O needs owned stable buffers. The full migration guide explains backend state, stream adapters and partial-write semantics.