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§
Sourcefn write_at(&self, _offset: u64, _buf: &[u8]) -> Result<()>
fn write_at(&self, _offset: u64, _buf: &[u8]) -> Result<()>
Write exactly buf.len() bytes at offset. Default: returns
Error::ReadOnly.
Sourcefn is_writable(&self) -> bool
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.
Sourcefn set_len(&self, _new_len: u64) -> Result<()>
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.
Sourcefn can_grow(&self) -> bool
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>
impl<T: BlockDevice + ?Sized> BlockDevice for Arc<T>
Source§impl<T: BlockDevice + ?Sized> BlockDevice for Box<T>
impl<T: BlockDevice + ?Sized> BlockDevice for Box<T>
Implementors§
impl BlockDevice for CachingDevice
impl BlockDevice for CallbackDevice
impl BlockDevice for FileDevice
impl BlockDevice for OwnedRwSlice
impl BlockDevice for OwnedSlice
Same rationale as SliceReader: read-only by default.
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.
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.