Skip to main content

BlockDevice

Trait BlockDevice 

Source
pub trait BlockDevice: BlockRead {
    // Provided methods
    fn write_at(&self, _offset: u64, _buf: &[u8]) -> Result<()> { ... }
    fn flush(&self) -> Result<()> { ... }
    fn is_writable(&self) -> bool { ... }
    fn set_len(&self, _new_len: u64) -> Result<()> { ... }
    fn can_grow(&self) -> bool { ... }
}
Expand description

Read-write random-access block device. Implementors that genuinely support writes override write_at / flush / is_writable. The defaults model a strict read-only device.

Provided Methods§

Source

fn write_at(&self, _offset: u64, _buf: &[u8]) -> Result<()>

Write exactly buf.len() bytes at offset. Default: returns Error::ReadOnly.

Source

fn flush(&self) -> Result<()>

Flush pending writes to stable storage. No-op by default.

Source

fn is_writable(&self) -> bool

Whether write_at is likely to succeed. Mount paths use this to decide whether to attempt journal replay or stay strict-read-only.

Source

fn set_len(&self, _new_len: u64) -> Result<()>

Set the device’s length to new_len. Default: returns Error::ReadOnly.

§WHY A DEVICE NEEDS THIS AT ALL

Every sparse disk-image format in this family allocates the same way: append a block, cluster or grain to the end of the file, then record where it went. The only tool any of them had for the first half was a write past the end, and #75 refused that — correctly. write_all at a seeked offset EXTENDS a file, so the backing store grew while size_bytes went on reporting its construction-time length, and a caller bounding its reads by size_bytes (which is what crate::CachingDevice does, clamping every block it fetches) could never reach the bytes it had just written. The two halves of one device disagreed about where it ended. That is rust-fs-core#70.

The bound is right and it stays. What was missing is the other half: a way to say make the device longer, out loud, so the number it reports and everything caching it move at the same moment. See rust-fs-core#147 and #129 for the four crates this blocked, and the measurements that named them.

§THE CONTRACT IS THE ATOMICITY, NOT THE SIGNATURE

An implementation must leave the backing store, the number BlockRead::size_bytes reports, and any cached view of the device agreeing with each other when this returns. An implementation that extends the store and leaves size_bytes stale, or leaves a cache holding a block clamped to the old length, has re-created #70 — and it will pass a naive test, because the write it enables succeeds and only a LATER cached read finds the hole.

Where the two steps cannot be made one instruction, publish the NARROWER of the two lengths first: the declared size may lag behind a store that has already grown, but it must never exceed one that has already shrunk. A read inside a device that is shorter than it claims is a read off the end of the store.

§IT SETS, IT DOES NOT ONLY GROW

new_len below the current length TRUNCATES, discarding the bytes past it, exactly as std::fs::File::set_len does — the name is that method’s and it means the same thing. A caller that only ever appends should compute new_len from size_bytes() and never hand this a smaller number; hiding the shrink behind a refusal would make a method named set_len do something else.

§WHY THIS IS ON BlockDevice AND NOT A GrowableDevice TRAIT

Because of what the consumers hold. All four image crates carry Arc<dyn BlockDevice> — rust-img-vhdx at 15 sites, rust-img-qcow2 at 7, rust-img-vhd at 6, rust-img-vmdk at 5 — and a dyn type cannot be bounded onto a second trait without downcasting through std::any::Any. That turns “this device cannot grow” from a value into a failed downcast: a runtime error with a worse message than the one the default below gives, at every one of those 33 sites.

It is also the idiom this trait already uses. write_at defaults to Err(Error::ReadOnly) and is_writable defaults to false for exactly the same reason — an optional capability, answered by the device rather than by the type system. can_grow is is_writable for length, and it is asked the same way.

§THE DEFAULT REFUSES, AND IT IS ReadOnly DELIBERATELY

Same variant as write_at’s default, because it is the same statement one field over: this device will not accept a mutation of this kind. crate::stream maps ReadOnly to PermissionDenied and Custom to an uncategorised io::Error::other, so a caller branching on the former to say “this cannot be modified” keeps working over a refused grow.

A device that is writable but fixed-length — a block device node, a slice — is the one place that reads oddly, and it is why can_grow exists: branch on the capability, not on the error.

Source

fn can_grow(&self) -> bool

Whether set_len is likely to succeed. Default: false.

The question an image writer asks before it plans an allocation, and the reason the default above can be a refusal rather than a panic or a downcast. is_writable for length.

false here is a promise that set_len will refuse, not a hint. A caller is entitled to read it once at mount time and build a writer around the answer.

Dyn Compatibility§

This trait is dyn compatible.

In older versions of Rust, dyn compatibility was called "object safety".

Implementations on Foreign Types§

Source§

impl<T: BlockDevice + ?Sized> BlockDevice for Arc<T>

Source§

fn write_at(&self, offset: u64, buf: &[u8]) -> Result<()>

Source§

fn flush(&self) -> Result<()>

Source§

fn is_writable(&self) -> bool

Source§

fn set_len(&self, new_len: u64) -> Result<()>

Source§

fn can_grow(&self) -> bool

Source§

impl<T: BlockDevice + ?Sized> BlockDevice for Box<T>

Source§

fn write_at(&self, offset: u64, buf: &[u8]) -> Result<()>

Source§

fn flush(&self) -> Result<()>

Source§

fn is_writable(&self) -> bool

Source§

fn set_len(&self, new_len: u64) -> Result<()>

Source§

fn can_grow(&self) -> bool

Implementors§

Source§

impl BlockDevice for CachingDevice

Source§

impl BlockDevice for CallbackDevice

Source§

impl BlockDevice for FileDevice

Source§

impl BlockDevice for OwnedRwSlice

Source§

impl BlockDevice for OwnedSlice

Same rationale as SliceReader: read-only by default.

Source§

impl<'a> BlockDevice for SliceReader<'a>

Slices are read-only by default — even where the parent is writable, slicing is almost always paired with a read-only inspection or dispatch workflow.

Source§

impl<T: BlockRead> BlockDevice for ReadOnlyDevice<T>

BlockDevice impl uses the trait’s default (Err(ReadOnly) for write_at, no-op flush, is_writable() -> false). Even if T implements BlockDevice with full writes, the wrapper hides that.