use crate::btree::{Tree, TreeGeometry};
use crate::chunk::{ChunkMap, DiskKey};
use crate::compression::{self, Compression};
use crate::dir::{self, DirEntry, DIR_INDEX_KEY, XATTR_ITEM_KEY};
use crate::error::{Error, Result};
use crate::inode::{Inode, FIRST_FREE_OBJECTID, INODE_ITEM_KEY};
use crate::superblock::{le64, Superblock};
use crate::xattr::{self, XattrEntry};
use fs_core::{BlockDevice, BlockRead};
use std::collections::BTreeMap;
use std::sync::Arc;
pub use crate::chunk::objectid::FS_TREE as FS_TREE_OBJECTID;
pub type RootTreeItem = (u64, u8, u64, Vec<u8>);
pub const ROOT_ITEM_KEY: u8 = 132;
pub const EXTENT_DATA_KEY: u8 = 108;
pub mod root_item {
pub const GENERATION: usize = 160;
pub const ROOT_DIRID: usize = 168;
pub const BYTENR: usize = 176;
pub const LAST_SNAPSHOT: usize = 200;
pub const FLAGS: usize = 208;
pub const LEVEL: usize = 238;
pub const GENERATION_V2: usize = 239;
pub const MIN_SIZE: usize = FLAGS + 8;
}
fn sector_aligned_span(logical: u64, len: u64, sector: u64) -> Option<(u64, u64)> {
let first = logical - logical % sector;
let end = logical.checked_add(len)?;
let last = end.div_ceil(sector).checked_mul(sector)?;
let span = last.checked_sub(first)?;
Some((first, span))
}
fn fs_and_csum_tree_roots(tree: &crate::btree::Tree, root_tree: u64) -> Result<(u64, Option<u64>)> {
Ok((
required_root_item_target(tree, root_tree, FS_TREE_OBJECTID)?,
root_item_target(tree, root_tree, crate::csum::CSUM_TREE_OBJECTID)?,
))
}
pub(crate) fn root_item_target(
tree: &crate::btree::Tree,
root_tree: u64,
objectid: u64,
) -> Result<Option<u64>> {
let mut root = None;
tree.for_each(root_tree, &mut |key: &DiskKey, data: &[u8]| {
if key.objectid == objectid
&& key.key_type == ROOT_ITEM_KEY
&& data.len() >= root_item::BYTENR + 8
{
root = Some(le64(data, root_item::BYTENR));
return Ok(false);
}
Ok(true)
})?;
Ok(root)
}
pub(crate) fn required_root_item_target(
tree: &crate::btree::Tree,
root_tree: u64,
objectid: u64,
) -> Result<u64> {
root_item_target(tree, root_tree, objectid)?.ok_or_else(|| {
Error::BadSuperblock(format!(
"the root tree holds no ROOT_ITEM for tree {objectid}"
))
})
}
#[allow(dead_code)]
pub(crate) mod file_extent {
pub const GENERATION: usize = 0;
pub const RAM_BYTES: usize = 8;
pub const COMPRESSION: usize = 16;
pub const ENCRYPTION: usize = 17;
pub const OTHER_ENCODING: usize = 18;
pub const TYPE: usize = 20;
pub const INLINE_DATA: usize = 21;
pub const DISK_BYTENR: usize = 21;
pub const DISK_NUM_BYTES: usize = 29;
pub const OFFSET: usize = 37;
pub const NUM_BYTES: usize = 45;
pub const REGULAR_SIZE: usize = 53;
}
const EXTENT_INLINE: u8 = 0;
pub(crate) const EXTENT_REGULAR: u8 = 1;
const EXTENT_PREALLOC: u8 = 2;
enum Piece<'a> {
Inline(std::borrow::Cow<'a, [u8]>),
Regular { logical: u64, len: u64 },
Compressed {
logical: u64,
disk_len: u64,
ram_len: u64,
offset: u64,
len: u64,
algo: Compression,
},
Hole,
Preallocated { logical: u64, len: u64 },
}
pub(crate) struct FileExtent {
pub start: u64,
pub len: u64,
pub logical: Option<u64>,
pub extent_start: u64,
pub compressed: bool,
}
pub struct Filesystem {
pub(crate) device: Arc<dyn BlockRead>,
pub(crate) devices: BTreeMap<u64, Arc<dyn BlockRead>>,
pub(crate) writable: Option<Arc<dyn BlockDevice>>,
pub(crate) sb: Superblock,
pub(crate) map: ChunkMap,
pub(crate) fs_tree_root: u64,
csum_tree_root: Option<u64>,
nodes: std::sync::Mutex<NodeCache>,
}
pub const NODE_CACHE_BLOCKS: usize = 1024;
pub(crate) struct NodeCache {
capacity: usize,
blocks: std::collections::HashMap<u64, (crate::btree::TreeBlock, u64)>,
tick: u64,
}
impl NodeCache {
fn new(capacity: usize) -> Self {
Self {
capacity,
blocks: std::collections::HashMap::new(),
tick: 0,
}
}
fn get(&mut self, logical: u64) -> Option<crate::btree::TreeBlock> {
self.tick += 1;
let tick = self.tick;
self.blocks.get_mut(&logical).map(|(block, used)| {
*used = tick;
block.clone()
})
}
fn put(&mut self, logical: u64, block: &crate::btree::TreeBlock) {
if self.capacity == 0 {
return;
}
if self.blocks.len() >= self.capacity && !self.blocks.contains_key(&logical) {
if let Some(&victim) = self
.blocks
.iter()
.min_by_key(|(_, (_, used))| *used)
.map(|(k, _)| k)
{
self.blocks.remove(&victim);
}
}
self.tick += 1;
self.blocks.insert(logical, (block.clone(), self.tick));
}
fn clear(&mut self) {
self.blocks.clear();
}
}
pub const MAX_SYMLINK_TARGET: u64 = 4096;
pub const MAX_WHOLE_FILE_READ: u64 = 1 << 30;
fn short_extent(ino: u64, kind: &str) -> Error {
Error::BadSuperblock(format!(
"inode {ino}: a {kind} extent is shorter than the range it covers"
))
}
pub const DEFAULT_CACHE_BLOCKS: usize = 0;
impl Filesystem {
pub fn mount(device: Arc<dyn BlockRead>) -> Result<Self> {
Self::mount_with_cache(device, DEFAULT_CACHE_BLOCKS)
}
pub fn mount_with_cache(device: Arc<dyn BlockRead>, blocks: usize) -> Result<Self> {
if blocks == 0 {
return Self::open(device, None);
}
let (sb, copy) = crate::superblock::read_superblock(&*device)?;
let sectorsize = u64::from(sb.sectorsize);
let device: Arc<dyn BlockRead> =
fs_core::CachingDevice::read_only(device, sectorsize, blocks);
Self::open_pool(device, BTreeMap::new(), None, Some((sb, copy)))
}
pub fn mount_rw(device: Arc<dyn BlockDevice>) -> Result<Self> {
if !device.is_writable() {
return Err(Error::ReadOnly);
}
let fs = Self::open(device.clone(), Some(device))?;
crate::superblock::refuse_unmaintained_compat_ro(fs.sb.compat_ro_flags)?;
if fs.sb.is_seeding() {
return Err(Error::UnsupportedFeature(
"this device is a seed for another filesystem, which can be read but not \
written; write to the filesystem sprouted from it"
.into(),
));
}
Ok(fs)
}
pub fn is_writable(&self) -> bool {
self.writable.is_some()
}
fn writable_span(device: &Arc<dyn BlockDevice>, physical: u64, len: usize) -> Result<()> {
let end = physical.checked_add(len as u64).ok_or_else(|| {
Error::BadSuperblock(format!(
"a chunk maps a write to {physical}, which ends past the address space"
))
})?;
let device_bytes = device.size_bytes();
if end > device_bytes {
return Err(Error::BadSuperblock(format!(
"a chunk maps a write to [{physical}, {end}) on a device of {device_bytes} bytes"
)));
}
Ok(())
}
pub(crate) fn mirror_spans(
device: &Arc<dyn BlockDevice>,
map: &ChunkMap,
logical: u64,
len: usize,
) -> Result<Vec<(u64, usize, usize)>> {
let mut spans = Vec::new();
for mirror in 0..map.mirrors_at(logical)? {
let mut done = 0usize;
while done < len {
let m = map.map_mirror(logical + done as u64, mirror)?;
let n = (m.len as usize).min(len - done);
if n == 0 {
return Err(Error::UnmappedLogical(logical + done as u64));
}
Self::writable_span(device, m.physical, n)?;
spans.push((m.physical, done, n));
done += n;
}
}
Ok(spans)
}
pub(crate) fn write_logical_all_mirrors(
device: &Arc<dyn BlockDevice>,
map: &ChunkMap,
logical: u64,
buf: &[u8],
) -> Result<()> {
for (physical, from, n) in Self::mirror_spans(device, map, logical, buf.len())? {
device.write_at(physical, &buf[from..from + n])?;
}
Ok(())
}
fn open(device: Arc<dyn BlockRead>, writable: Option<Arc<dyn BlockDevice>>) -> Result<Self> {
Self::open_pool(device, BTreeMap::new(), writable, None)
}
pub fn mount_pool(devices: Vec<Arc<dyn BlockRead>>) -> Result<Self> {
if devices.is_empty() {
return Err(Error::UnsupportedFeature(
"a pool needs at least one device".to_string(),
));
}
let mut by_id: BTreeMap<u64, Arc<dyn BlockRead>> = BTreeMap::new();
let mut fsid: Option<[u8; 16]> = None;
for dev in devices {
let (sb, _) = crate::superblock::read_superblock(&*dev)?;
match fsid {
None => fsid = Some(sb.fsid),
Some(seen) if seen != sb.fsid => {
return Err(Error::UnsupportedFeature(
"these devices belong to different filesystems".to_string(),
))
}
Some(_) => {}
}
let id = sb.dev_item.devid;
if by_id.insert(id, dev).is_some() {
return Err(Error::UnsupportedFeature(format!(
"two devices both claim to be device {id}"
)));
}
}
let first = by_id
.values()
.next()
.expect("at least one device, checked above")
.clone();
Self::open_pool(first, by_id, None, None)
}
fn open_pool(
device: Arc<dyn BlockRead>,
devices: BTreeMap<u64, Arc<dyn BlockRead>>,
writable: Option<Arc<dyn BlockDevice>>,
known: Option<(crate::superblock::Superblock, usize)>,
) -> Result<Self> {
let (sb, copy) = match known {
Some(selection) if writable.is_none() => selection,
_ => crate::superblock::read_superblock(&*device)?,
};
if writable.is_some() && copy != 0 {
return Err(Error::UnsupportedFeature(format!(
"the primary superblock is damaged or older than copy {copy}; the volume can be \
mounted read-only from that copy, and should be checked before it is written"
)));
}
if sb.num_devices > 1 && devices.len() as u64 != sb.num_devices {
return Err(Error::UnsupportedFeature(format!(
"this filesystem spans {} devices and {} {} given; reading one of them \
alone would return the wrong data rather than fail. Open it with \
`mount_pool`, giving every device.",
sb.num_devices,
devices.len(),
if devices.len() == 1 { "was" } else { "were" }
)));
}
if sb.has_dirty_log() {
return Err(Error::DirtyLog);
}
if sb.is_metadump() {
return Err(Error::UnsupportedFeature(
"this image is a metadata-only dump (btrfs-image): its data extents are \
absent, so no file in it can be read"
.into(),
));
}
let boot = ChunkMap::bootstrap(&sb)?;
let mut map = boot.clone();
{
let read = |logical: u64, buf: &mut [u8]| -> Result<()> {
Self::read_logical_pool(&device, &devices, &boot, logical, buf)
};
let mirrors = |logical: u64| -> Result<usize> { boot.mirrors_at(logical) };
let read_mirror = |logical: u64, mirror: usize, buf: &mut [u8]| -> Result<()> {
Self::read_logical_pool_mirror(&device, &devices, &boot, mirror, logical, buf)
};
let tree = Tree::from_superblock(&sb, &read).with_redundancy(&mirrors, &read_mirror);
let mut found = Vec::new();
tree.for_each(sb.chunk_root, &mut |key: &DiskKey, data: &[u8]| {
if let Some(chunk) = crate::chunk::chunk_from_item(key, data, sb.sectorsize)? {
found.push(chunk);
}
Ok(true)
})?;
for chunk in found {
match map.chunk_for(chunk.logical) {
Some(existing) if *existing == chunk => continue,
_ => map.insert(chunk)?,
}
}
}
let (fs_tree_root, csum_tree_root) = {
let read = |logical: u64, buf: &mut [u8]| -> Result<()> {
Self::read_logical_pool(&device, &devices, &map, logical, buf)
};
let mirrors = |logical: u64| -> Result<usize> { map.mirrors_at(logical) };
let read_mirror = |logical: u64, mirror: usize, buf: &mut [u8]| -> Result<()> {
Self::read_logical_pool_mirror(&device, &devices, &map, mirror, logical, buf)
};
let tree = Tree::from_superblock(&sb, &read).with_redundancy(&mirrors, &read_mirror);
fs_and_csum_tree_roots(&tree, sb.root)?
};
let fs = Filesystem {
device,
devices,
writable,
sb,
map,
fs_tree_root,
csum_tree_root,
nodes: std::sync::Mutex::new(NodeCache::new(NODE_CACHE_BLOCKS)),
};
Ok(fs)
}
pub(crate) fn pool_reader(&self) -> PoolReader<'_> {
PoolReader {
geom: crate::btree::TreeGeometry::from_superblock(&self.sb),
read: Box::new(move |logical, buf| {
Self::read_logical_pool(&self.device, &self.devices, &self.map, logical, buf)
}),
mirrors: Box::new(move |logical| self.map.mirrors_at(logical)),
read_mirror: Box::new(move |logical, mirror, buf| {
Self::read_logical_pool_mirror(
&self.device,
&self.devices,
&self.map,
mirror,
logical,
buf,
)
}),
}
}
pub(crate) fn read_logical_pool(
device: &Arc<dyn BlockRead>,
devices: &BTreeMap<u64, Arc<dyn BlockRead>>,
map: &ChunkMap,
logical: u64,
buf: &mut [u8],
) -> Result<()> {
Self::read_logical_pool_mirror(device, devices, map, 0, logical, buf)
}
pub(crate) fn read_logical_pool_mirror(
device: &Arc<dyn BlockRead>,
devices: &BTreeMap<u64, Arc<dyn BlockRead>>,
map: &ChunkMap,
mirror: usize,
logical: u64,
buf: &mut [u8],
) -> Result<()> {
if devices.is_empty() {
return Self::read_logical_on_mirror(device, map, u64::MAX, mirror, logical, buf);
}
let mut done = 0usize;
while done < buf.len() {
let m = map.map_mirror(logical + done as u64, mirror)?;
let n = (m.len as usize).min(buf.len() - done);
if n == 0 {
return Err(Error::UnmappedLogical(logical + done as u64));
}
let dev = devices.get(&m.devid).ok_or_else(|| {
Error::UnsupportedFeature(format!(
"the range at {} lives on device {}, which was not given",
logical + done as u64,
m.devid
))
})?;
dev.read_at(m.physical, &mut buf[done..done + n])?;
done += n;
}
Ok(())
}
pub(crate) fn read_logical_on_mirror(
device: &Arc<dyn BlockRead>,
map: &ChunkMap,
devid: u64,
mirror: usize,
logical: u64,
buf: &mut [u8],
) -> Result<()> {
let mut done = 0usize;
while done < buf.len() {
let m = map.map_mirror(logical + done as u64, mirror)?;
if devid != u64::MAX && m.devid != devid {
return Err(Error::UnsupportedFeature(format!(
"the range at {} lives on device {} and this filesystem was opened \
with device {devid} alone; reading it from the wrong device would \
return the wrong data rather than fail",
logical + done as u64,
m.devid
)));
}
let n = (m.len as usize).min(buf.len() - done);
if n == 0 {
return Err(Error::UnmappedLogical(logical + done as u64));
}
device.read_at(m.physical, &mut buf[done..done + n])?;
done += n;
}
Ok(())
}
pub(crate) fn reroot(&self, fs_tree_root: u64) -> Result<Self> {
let fs = Filesystem {
device: self.device.clone(),
devices: self.devices.clone(),
writable: None,
sb: self.sb.clone(),
map: self.map.clone(),
fs_tree_root,
csum_tree_root: self.csum_tree_root,
nodes: std::sync::Mutex::new(NodeCache::new(NODE_CACHE_BLOCKS)),
};
Ok(fs)
}
fn with_fs_tree<R>(&self, f: impl FnOnce(&Tree, u64) -> Result<R>) -> Result<R> {
let read = |logical: u64, buf: &mut [u8]| -> Result<()> {
Self::read_logical_pool(&self.device, &self.devices, &self.map, logical, buf)
};
let mirrors = |logical: u64| -> Result<usize> { self.map.mirrors_at(logical) };
let read_mirror = |logical: u64, mirror: usize, buf: &mut [u8]| -> Result<()> {
Self::read_logical_pool_mirror(
&self.device,
&self.devices,
&self.map,
mirror,
logical,
buf,
)
};
let get = |logical: u64| self.nodes.lock().expect("node cache poisoned").get(logical);
let put = |logical: u64, block: &crate::btree::TreeBlock| {
self.nodes
.lock()
.expect("node cache poisoned")
.put(logical, block)
};
let tree = Tree::new(TreeGeometry::from_superblock(&self.sb), &read)
.with_redundancy(&mirrors, &read_mirror)
.with_cache(&get, &put);
f(&tree, self.fs_tree_root)
}
fn item(&self, key: (u64, u8, u64)) -> Result<Option<Vec<u8>>> {
let key = DiskKey {
objectid: key.0,
key_type: key.1,
offset: key.2,
};
self.with_fs_tree(|tree, root| Ok(tree.search(root, &key)?.map(|item| item.data)))
}
#[allow(clippy::type_complexity)]
fn item_run(&self, objectid: u64, key_type: u8) -> Result<Vec<((u64, u8, u64), Vec<u8>)>> {
self.with_fs_tree(|tree, root| {
Ok(tree
.find_all(root, objectid, key_type)?
.into_iter()
.map(|item| {
(
(item.key.objectid, item.key.key_type, item.key.offset),
item.data,
)
})
.collect())
})
}
pub(crate) fn forget_tree_blocks(&self) {
self.nodes.lock().expect("node cache poisoned").clear();
}
pub fn read_tree_block(&self, logical: u64) -> Result<crate::btree::TreeBlock> {
let reader = self.pool_reader();
reader.tree().read_block(logical)
}
pub fn chunk_map(&self) -> &ChunkMap {
&self.map
}
pub fn superblock(&self) -> &Superblock {
&self.sb
}
pub fn root_tree_items(&self) -> Result<Vec<RootTreeItem>> {
let reader = self.pool_reader();
let tree = reader.tree();
let mut out = Vec::new();
tree.for_each(self.sb.root, &mut |key: &DiskKey, data: &[u8]| {
out.push((key.objectid, key.key_type, key.offset, data.to_vec()));
Ok(true)
})?;
Ok(out)
}
pub fn read_inode(&self, ino: u64) -> Result<Inode> {
let data = self
.item((ino, INODE_ITEM_KEY, 0))?
.ok_or(Error::NotFound)?;
Inode::parse(&data, ino)
}
pub fn root_inode(&self) -> Result<Inode> {
self.read_inode(FIRST_FREE_OBJECTID)
}
pub fn read_dir(&self, ino: u64) -> Result<Vec<DirEntry>> {
let inode = self.read_inode(ino)?;
if !inode.is_dir() {
return Err(Error::NotADirectory);
}
let mut out = Vec::new();
for ((objectid, key_type, _), data) in &self.item_run(ino, DIR_INDEX_KEY)? {
if *objectid != ino || *key_type != DIR_INDEX_KEY {
break;
}
for e in dir::parse_dir_items(data)? {
if e.name != b"." && e.name != b".." {
out.push(e);
}
}
}
Ok(out)
}
pub fn lookup(&self, dir_ino: u64, name: &[u8]) -> Result<Inode> {
let hit = self.lookup_entry(dir_ino, name)?;
if !hit.is_inode() {
return Err(Error::UnsupportedFeature(format!(
"{:?} names subvolume {} rather than an inode in this tree — resolve \
the path with `resolve_path`, which crosses into it",
String::from_utf8_lossy(name),
hit.ino
)));
}
self.read_inode(hit.ino)
}
pub(crate) fn lookup_entry(&self, dir_ino: u64, name: &[u8]) -> Result<DirEntry> {
if !self.read_inode(dir_ino)?.is_dir() {
return Err(Error::NotADirectory);
}
if name == b"." || name == b".." {
return Err(Error::NotFound);
}
let data = self
.item((dir_ino, dir::DIR_ITEM_KEY, dir::name_hash(name)))?
.ok_or(Error::NotFound)?;
dir::parse_dir_items(&data)?
.into_iter()
.find(|e| e.name == name)
.ok_or(Error::NotFound)
}
pub fn list_xattrs(&self, ino: u64) -> Result<Vec<XattrEntry>> {
self.read_inode(ino)?;
let mut out = Vec::new();
for ((objectid, key_type, _), data) in &self.item_run(ino, XATTR_ITEM_KEY)? {
if *objectid != ino || *key_type != XATTR_ITEM_KEY {
break;
}
out.extend(xattr::parse_xattr_items(data)?);
}
Ok(out)
}
pub fn get_xattr(&self, ino: u64, name: &[u8]) -> Result<Option<Vec<u8>>> {
self.read_inode(ino)?;
let Some(data) = self.item((ino, XATTR_ITEM_KEY, dir::name_hash(name)))? else {
return Ok(None);
};
Ok(xattr::parse_xattr_items(&data)?
.into_iter()
.find(|e| e.name == name)
.map(|e| e.value))
}
pub fn lookup_path(&self, path: &str) -> Result<Inode> {
self.lookup_path_bytes(path.as_bytes())
}
pub fn lookup_path_bytes(&self, path: &[u8]) -> Result<Inode> {
let mut inode = self.root_inode()?;
for component in path
.split(|&b| b == b'/')
.filter(|c| !c.is_empty() && *c != b".")
{
if component == b".." {
return Err(Error::UnsupportedFeature(
"`..` in a path is not resolved by lookup_path".into(),
));
}
if !inode.is_dir() {
return Err(Error::NotADirectory);
}
inode = self.lookup(inode.ino, component)?;
}
Ok(inode)
}
fn refuse_window_outside_extent(
&self,
ino: u64,
disk_bytenr: u64,
offset: u64,
len: u64,
) -> Result<()> {
if disk_bytenr == 0 {
return Ok(());
}
let (_, extent_len, _) = self.extent_item(disk_bytenr)?;
let start = disk_bytenr.checked_add(offset).ok_or_else(|| {
Error::BadSuperblock(format!(
"inode {ino}: an extent item at {disk_bytenr} has an offset of {offset}, \
which is past the end of the address space"
))
})?;
if !crate::write::window_inside_extent(start, len, disk_bytenr, extent_len) {
return Err(Error::BadSuperblock(format!(
"inode {ino}: an extent item covers [{start}, +{len}), which is outside the \
extent at {disk_bytenr} that the extent tree records as {extent_len} bytes"
)));
}
Ok(())
}
fn decode_extent<'a>(&self, data: &'a [u8], ino: u64) -> Result<Piece<'a>> {
if data.len() < file_extent::TYPE + 1 {
return Err(Error::BadSuperblock(format!(
"inode {ino}: extent item is {} bytes, too short to hold a type",
data.len()
)));
}
let _generation = le64(data, file_extent::GENERATION);
let ram_bytes = le64(data, file_extent::RAM_BYTES);
let compression = data[file_extent::COMPRESSION];
let encryption = data[file_extent::ENCRYPTION];
let other = u16::from_le_bytes(
data[file_extent::OTHER_ENCODING..file_extent::OTHER_ENCODING + 2]
.try_into()
.expect("2 bytes"),
);
let kind = data[file_extent::TYPE];
let algo = Compression::from_byte(compression)
.map_err(|e| Error::UnsupportedFeature(format!("inode {ino}: {e}")))?;
if encryption != 0 || other != 0 {
return Err(Error::UnsupportedFeature(format!(
"inode {ino}: extent is encoded (encryption {encryption}, other {other})"
)));
}
match kind {
EXTENT_INLINE => {
let end = data.len();
let start = file_extent::INLINE_DATA.min(end);
let raw = &data[start..end];
Ok(Piece::Inline(if algo.is_compressed() {
std::borrow::Cow::Owned(compression::decompress(
algo,
raw,
ram_bytes as usize,
self.sb.sectorsize as usize,
compression::ShortDecode::Pad,
)?)
} else {
std::borrow::Cow::Borrowed(raw)
}))
}
EXTENT_REGULAR | EXTENT_PREALLOC => {
if data.len() < file_extent::REGULAR_SIZE {
return Err(Error::BadSuperblock(format!(
"inode {ino}: non-inline extent item is {} bytes, need {}",
data.len(),
file_extent::REGULAR_SIZE
)));
}
let disk_bytenr = le64(data, file_extent::DISK_BYTENR);
let offset = le64(data, file_extent::OFFSET);
let num_bytes = le64(data, file_extent::NUM_BYTES);
let sector = u64::from(self.sb.sectorsize.max(1));
for (field, value) in [
("ram_bytes", ram_bytes),
("disk_bytenr", disk_bytenr),
("disk_num_bytes", le64(data, file_extent::DISK_NUM_BYTES)),
("offset", offset),
("num_bytes", num_bytes),
] {
if value % sector != 0 {
return Err(Error::BadSuperblock(format!(
"inode {ino}: extent item {field} {value} is not a multiple of \
the {sector}-byte sector"
)));
}
}
if disk_bytenr == 0 {
return Ok(Piece::Hole);
}
if kind == EXTENT_PREALLOC {
return Ok(Piece::Preallocated {
logical: disk_bytenr,
len: num_bytes,
});
}
let window_end = offset
.checked_add(num_bytes)
.filter(|end| *end <= ram_bytes);
if window_end.is_none() {
return Err(Error::BadSuperblock(format!(
"inode {ino}: an extent item covers [{offset}, +{num_bytes}) of an \
extent that is {ram_bytes} bytes long"
)));
}
if algo.is_compressed() {
let disk_len = le64(data, file_extent::DISK_NUM_BYTES);
if disk_len > compression::MAX_COMPRESSED {
return Err(Error::BadSuperblock(format!(
"inode {ino}: compressed extent occupies {disk_len} bytes on \
disk, more than the {} a compressed extent can",
compression::MAX_COMPRESSED
)));
}
self.refuse_window_outside_extent(ino, disk_bytenr, 0, disk_len)?;
return Ok(Piece::Compressed {
logical: disk_bytenr,
disk_len,
ram_len: ram_bytes,
offset,
len: num_bytes,
algo,
});
}
self.refuse_window_outside_extent(ino, disk_bytenr, offset, num_bytes)?;
Ok(Piece::Regular {
logical: disk_bytenr.checked_add(offset).ok_or_else(|| {
Error::BadSuperblock(format!(
"inode {ino}: extent at {disk_bytenr} plus offset {offset} \
leaves the address space"
))
})?,
len: num_bytes,
})
}
other => Err(Error::BadSuperblock(format!(
"inode {ino}: extent type {other} is not a defined value (ram_bytes {ram_bytes})"
))),
}
}
pub(crate) fn file_extents(&self, ino: u64) -> Result<Vec<FileExtent>> {
let mut out = Vec::new();
for ((objectid, key_type, offset), data) in &self.item_run(ino, EXTENT_DATA_KEY)? {
if *objectid != ino || *key_type != EXTENT_DATA_KEY {
break;
}
let start = *offset;
match self.decode_extent(data, ino)? {
Piece::Inline(bytes) => out.push(FileExtent {
start,
len: bytes.len() as u64,
logical: None,
extent_start: 0,
compressed: false,
}),
Piece::Preallocated { logical, len } => out.push(FileExtent {
start,
len,
logical: None,
extent_start: logical,
compressed: false,
}),
Piece::Hole => {}
Piece::Regular { logical, len } => out.push(FileExtent {
start,
len,
logical: Some(logical),
extent_start: le64(data, file_extent::DISK_BYTENR),
compressed: false,
}),
Piece::Compressed { len, .. } => out.push(FileExtent {
start,
len,
logical: None,
extent_start: le64(data, file_extent::DISK_BYTENR),
compressed: true,
}),
}
}
Ok(out)
}
pub fn read_file(&self, ino: u64) -> Result<Vec<u8>> {
let inode = self.read_inode(ino)?;
if !inode.is_regular_file() && !inode.is_symlink() {
return Err(Error::NotAFile);
}
if inode.size > self.sb.total_bytes {
return Err(Error::BadSuperblock(format!(
"inode {ino} says it is {} bytes, more than the {} the filesystem holds",
inode.size, self.sb.total_bytes
)));
}
let too_large = || {
Error::UnsupportedFeature(format!(
"inode {ino} is {} bytes, more than a whole-file read will hold in memory; \
read it in pieces with read_at",
inode.size
))
};
if inode.size > MAX_WHOLE_FILE_READ {
return Err(too_large());
}
let len = usize::try_from(inode.size).map_err(|_| too_large())?;
let mut out = Vec::new();
out.try_reserve_exact(len).map_err(|_| too_large())?;
out.resize(len, 0);
self.read_range(&inode, 0, &mut out)?;
Ok(out)
}
fn read_data_verified(&self, logical: u64, dst: &mut [u8], verify: bool) -> Result<()> {
if !verify || dst.is_empty() {
return Self::read_logical_pool(&self.device, &self.devices, &self.map, logical, dst);
}
let Some(csum_root) = self.csum_tree_root else {
return Self::read_logical_pool(&self.device, &self.devices, &self.map, logical, dst);
};
let sector = u64::from(self.sb.sectorsize);
if sector == 0 {
return Self::read_logical_pool(&self.device, &self.devices, &self.map, logical, dst);
}
let (first, span) = sector_aligned_span(logical, dst.len() as u64, sector)
.ok_or(Error::UnmappedLogical(logical))?;
let mut whole = vec![0u8; span as usize];
Self::read_logical_pool(&self.device, &self.devices, &self.map, first, &mut whole)?;
let digests = {
let reader = self.pool_reader();
let tree = reader.tree();
crate::csum::digests_for_range(
&tree,
csum_root,
self.sb.csum_type.digest_len(),
sector,
first,
span,
)?
};
for (i, chunk) in whole.chunks_exact(sector as usize).enumerate() {
let at = first + i as u64 * sector;
let Some(expected) = digests.get(&at) else {
continue;
};
if !self.sb.csum_type.verify(chunk, expected) {
return Err(Error::ChecksumMismatch {
what: "a data extent",
offset: at,
});
}
}
let skip = (logical - first) as usize;
dst.copy_from_slice(&whole[skip..skip + dst.len()]);
Ok(())
}
fn read_range(&self, inode: &Inode, from: u64, buf: &mut [u8]) -> Result<()> {
buf.fill(0);
if buf.is_empty() {
return Ok(());
}
let ino = inode.ino;
let verify =
inode.flags & (crate::write::INODE_NODATASUM | crate::write::INODE_NODATACOW) == 0;
let want_end = from.saturating_add(buf.len() as u64).min(inode.size);
for ((objectid, key_type, offset), data) in &self.item_run(ino, EXTENT_DATA_KEY)? {
if *objectid != ino || *key_type != EXTENT_DATA_KEY {
break;
}
let at = *offset;
if at >= want_end {
break;
}
let piece = self.decode_extent(data, ino)?;
let extent_len = match &piece {
Piece::Inline(bytes) => bytes.len() as u64,
Piece::Hole | Piece::Preallocated { .. } => continue,
Piece::Regular { len, .. } | Piece::Compressed { len, .. } => *len,
};
let start = at.max(from);
let end = at.saturating_add(extent_len).min(want_end);
if end <= start {
continue;
}
let take = (end - start) as usize;
let skip = (start - at) as usize;
let dst = &mut buf[(start - from) as usize..(end - from) as usize];
match piece {
Piece::Inline(bytes) => {
let src = bytes
.get(skip..skip + take)
.ok_or_else(|| short_extent(ino, "inline"))?;
dst.copy_from_slice(src);
}
Piece::Hole | Piece::Preallocated { .. } => unreachable!("handled above"),
Piece::Regular { logical, .. } => {
let at_logical = logical
.checked_add(skip as u64)
.ok_or_else(|| short_extent(ino, "regular"))?;
self.read_data_verified(at_logical, dst, verify)?;
}
Piece::Compressed {
logical,
disk_len,
ram_len,
offset: within,
algo,
..
} => {
let mut packed = vec![0u8; disk_len as usize];
self.read_data_verified(logical, &mut packed, verify)?;
let short = if at.saturating_add(ram_len) >= inode.size {
compression::ShortDecode::Pad
} else {
compression::ShortDecode::Refuse
};
let decoded = compression::decompress(
algo,
&packed,
ram_len as usize,
self.sb.sectorsize as usize,
short,
)?;
let src = usize::try_from(within)
.ok()
.and_then(|w| w.checked_add(skip))
.and_then(|at| Some(at..at.checked_add(take)?))
.and_then(|range| decoded.get(range))
.ok_or_else(|| short_extent(ino, "compressed"))?;
dst.copy_from_slice(src);
}
}
}
Ok(())
}
pub fn read_at(&self, ino: u64, offset: u64, buf: &mut [u8]) -> Result<usize> {
let inode = self.read_inode(ino)?;
if !inode.is_regular_file() && !inode.is_symlink() {
return Err(Error::NotAFile);
}
if offset >= inode.size {
return Ok(0);
}
let n = buf.len().min((inode.size - offset) as usize);
self.read_range(&inode, offset, &mut buf[..n])?;
Ok(n)
}
pub fn read_link(&self, ino: u64) -> Result<Vec<u8>> {
let inode = self.read_inode(ino)?;
if !inode.is_symlink() {
return Err(Error::NotAFile);
}
if inode.size > MAX_SYMLINK_TARGET {
return Err(Error::BadSuperblock(format!(
"inode {ino}: symlink target of {} bytes is longer than any path",
inode.size
)));
}
self.read_file(ino)
}
pub fn list_path(&self, path: &str) -> Result<Vec<DirEntry>> {
self.list_path_bytes(path.as_bytes())
}
pub fn list_path_bytes(&self, path: &[u8]) -> Result<Vec<DirEntry>> {
let target = self.resolve_path_bytes(path)?;
if target.inode.ino == crate::subvol::EMPTY_SUBVOL_DIR_OBJECTID {
return Ok(Vec::new());
}
target.fs(self).read_dir(target.inode.ino)
}
pub fn read_path(&self, path: &str) -> Result<Vec<u8>> {
let target = self.resolve_path(path)?;
target.fs(self).read_file(target.inode.ino)
}
}
pub(crate) struct PoolReader<'a> {
geom: crate::btree::TreeGeometry,
read: OwnedReadBlock<'a>,
mirrors: OwnedMirrorCount<'a>,
read_mirror: OwnedReadMirror<'a>,
}
type OwnedReadBlock<'a> = Box<dyn Fn(u64, &mut [u8]) -> Result<()> + 'a>;
type OwnedMirrorCount<'a> = Box<dyn Fn(u64) -> Result<usize> + 'a>;
type OwnedReadMirror<'a> = Box<dyn Fn(u64, usize, &mut [u8]) -> Result<()> + 'a>;
impl PoolReader<'_> {
pub(crate) fn tree(&self) -> crate::btree::Tree<'_> {
crate::btree::Tree::new(self.geom, &*self.read)
.with_redundancy(&*self.mirrors, &*self.read_mirror)
}
}
#[cfg(test)]
mod root_item_target_tests {
use super::*;
use crate::btree::test_blocks::{geom, key, leaf, LEAF_A, NODESIZE};
use crate::btree::Tree;
fn minimal_root_item(bytenr: u64) -> Vec<u8> {
let mut body = vec![0u8; root_item::BYTENR + 8];
body[root_item::BYTENR..root_item::BYTENR + 8].copy_from_slice(&bytenr.to_le_bytes());
body
}
fn tree_over(block: Vec<u8>) -> (Vec<u8>, u64) {
(block, LEAF_A)
}
fn lookup(entries: &[(DiskKey, Vec<u8>)], objectid: u64) -> Result<Option<u64>> {
let (block, at) = tree_over(leaf(LEAF_A, crate::chunk::objectid::ROOT_TREE, entries));
let read = |logical: u64, buf: &mut [u8]| -> Result<()> {
assert_eq!(logical, at, "the walk asked for a block that is not there");
buf.copy_from_slice(&block[..buf.len()]);
Ok(())
};
let tree = Tree::new(geom(), &read);
root_item_target(&tree, at, objectid)
}
fn required_lookup(entries: &[(DiskKey, Vec<u8>)], objectid: u64) -> Result<u64> {
let (block, at) = tree_over(leaf(LEAF_A, crate::chunk::objectid::ROOT_TREE, entries));
let read = |logical: u64, buf: &mut [u8]| -> Result<()> {
assert_eq!(logical, at, "the walk asked for a block that is not there");
buf.copy_from_slice(&block[..buf.len()]);
Ok(())
};
let tree = Tree::new(geom(), &read);
required_root_item_target(&tree, at, objectid)
}
#[test]
fn a_root_item_long_enough_to_answer_from_is_accepted() {
let entries = vec![(key(7, ROOT_ITEM_KEY, 0), minimal_root_item(0xABCD_0000))];
assert_eq!(lookup(&entries, 7).unwrap(), Some(0xABCD_0000));
}
#[test]
fn a_root_item_one_byte_too_short_is_not_used() {
let mut short = minimal_root_item(0xABCD_0000);
short.pop();
let entries = vec![(key(7, ROOT_ITEM_KEY, 0), short)];
assert_eq!(
lookup(&entries, 7).unwrap(),
None,
"a body that cannot hold the field must not be read from"
);
}
#[test]
fn the_first_matching_root_item_is_the_answer() {
let entries = vec![
(key(7, ROOT_ITEM_KEY, 0), minimal_root_item(0x1111_0000)),
(key(7, ROOT_ITEM_KEY, 1), minimal_root_item(0x2222_0000)),
];
assert_eq!(lookup(&entries, 7).unwrap(), Some(0x1111_0000));
}
#[test]
fn a_missing_root_item_is_ok_none_not_an_error() {
let entries = vec![(key(7, ROOT_ITEM_KEY, 0), minimal_root_item(1))];
assert_eq!(
lookup(&entries, 9).unwrap(),
None,
"root_item_target must not itself decide that absence is fatal"
);
}
#[test]
fn required_root_item_target_turns_absence_into_an_error_naming_the_tree() {
let entries = vec![(key(7, ROOT_ITEM_KEY, 0), minimal_root_item(1))];
let err = required_lookup(&entries, 9).unwrap_err();
assert!(
err.to_string().contains('9'),
"the error should name the tree that is missing, got: {err}"
);
}
#[test]
fn a_real_walk_failure_is_not_confused_with_absence() {
let read = |_logical: u64, _buf: &mut [u8]| -> Result<()> {
Err(Error::Io("simulated device failure".to_string()))
};
let tree = Tree::new(geom(), &read);
let plain = root_item_target(&tree, LEAF_A, 9);
assert!(
plain.is_err(),
"a walk failure must be Err, not Ok(None) -- otherwise it is \
indistinguishable from a tree that simply has no ROOT_ITEM"
);
let required = required_root_item_target(&tree, LEAF_A, 9);
assert!(
required.is_err(),
"and the required form must not turn that Err into its own \
'tree is absent' message, losing the original cause"
);
}
#[test]
fn only_root_items_are_considered() {
let entries = vec![(key(7, ROOT_ITEM_KEY + 1, 0), minimal_root_item(0x3333_0000))];
assert_eq!(
lookup(&entries, 7).unwrap(),
None,
"an item of the wrong type must not be mistaken for a match"
);
}
#[test]
fn the_fixture_leaf_has_room_for_two_root_items() {
assert!(NODESIZE as usize > 2 * (root_item::BYTENR + 8) + 128);
}
}
#[cfg(test)]
mod sector_aligned_span_tests {
use super::sector_aligned_span;
#[test]
fn a_read_inside_one_sector_spans_exactly_that_sector() {
let sector = 4096u64;
assert_eq!(sector_aligned_span(100, 16, sector), Some((0, 4096)));
}
#[test]
fn a_read_crossing_a_boundary_spans_both_sectors() {
let sector = 4096u64;
assert_eq!(sector_aligned_span(4090, 16, sector), Some((0, 8192)));
}
#[test]
fn an_overflowing_add_is_refused() {
assert_eq!(sector_aligned_span(u64::MAX - 4, 16, 4096), None);
}
#[test]
fn a_round_up_that_overflows_the_multiplication_is_refused_not_wrapped() {
let sector = 4096u64;
let logical = u64::MAX - 4096;
let len = 4086u64;
assert_eq!(
sector_aligned_span(logical, len, sector),
None,
"the multiplication overflowed and must not silently wrap"
);
}
#[test]
fn a_small_first_does_not_let_the_multiplication_wrap_through_uncaught() {
let sector = 4096u64;
assert_eq!(
sector_aligned_span(0, u64::MAX, sector),
None,
"logical=0 with an enormous len drives the same round-up \
overflow while first stays small, so a wrapped `last` can \
land at or above `first` and slip past a bare subtraction \
undetected -- this must still be refused"
);
}
#[test]
fn every_end_near_u64_max_that_would_overflow_the_round_up_is_refused() {
let sector = 4096u64;
for len in 0..=(2 * sector) {
let logical = u64::MAX - len;
let result = sector_aligned_span(logical, 1, sector);
if let Some((first, span)) = result {
assert!(
first <= logical,
"first ({first:#x}) must not be after logical ({logical:#x})"
);
assert!(
first.checked_add(span).is_some(),
"a returned (first, span) must not itself overflow when added back \
together: first={first:#x} span={span:#x}"
);
}
}
}
#[test]
fn the_span_always_covers_the_request() {
for (logical, len, sector) in [
(0u64, 0u64, 512u64),
(1, 1, 512),
(511, 2, 512),
(512, 1, 512),
] {
let Some((first, span)) = sector_aligned_span(logical, len, sector) else {
continue;
};
assert!(first <= logical);
assert!(
first + span >= logical + len,
"span [{first}, {}) does not cover the request [{logical}, {})",
first + span,
logical + len
);
}
}
}
#[cfg(test)]
mod fs_and_csum_tree_roots_tests {
use super::*;
use crate::btree::test_blocks::{geom, key, leaf, node, LEAF_A, LEAF_B, NODESIZE, ROOT};
use crate::btree::Tree;
use std::collections::HashMap;
fn minimal_root_item(bytenr: u64) -> Vec<u8> {
let mut body = vec![0u8; root_item::BYTENR + 8];
body[root_item::BYTENR..root_item::BYTENR + 8].copy_from_slice(&bytenr.to_le_bytes());
body
}
fn fs_tree_root_only(leaf_b: Vec<u8>) -> (HashMap<u64, Vec<u8>>, u64) {
let a = leaf(
LEAF_A,
crate::chunk::objectid::ROOT_TREE,
&[(
key(FS_TREE_OBJECTID, ROOT_ITEM_KEY, 0),
minimal_root_item(0xF00D),
)],
);
let r = node(
ROOT,
crate::chunk::objectid::ROOT_TREE,
1,
&[
(key(FS_TREE_OBJECTID, ROOT_ITEM_KEY, 0), LEAF_A),
(
key(crate::csum::CSUM_TREE_OBJECTID, ROOT_ITEM_KEY, 0),
LEAF_B,
),
],
);
(
HashMap::from([(LEAF_A, a), (LEAF_B, leaf_b), (ROOT, r)]),
ROOT,
)
}
#[test]
fn a_genuinely_absent_csum_tree_is_ok_none() {
let empty_b = leaf(LEAF_B, crate::chunk::objectid::ROOT_TREE, &[]);
let (blocks, root) = fs_tree_root_only(empty_b);
let read = move |logical: u64, buf: &mut [u8]| -> Result<()> {
let block = blocks
.get(&logical)
.unwrap_or_else(|| panic!("no block at {logical:#x} in this fixture"));
buf.copy_from_slice(&block[..buf.len()]);
Ok(())
};
let tree = Tree::new(geom(), &read);
let (fs_root, csum_root) = fs_and_csum_tree_roots(&tree, root).unwrap();
assert_eq!(fs_root, 0xF00D);
assert_eq!(
csum_root, None,
"no ROOT_ITEM for the csum tree anywhere in this fixture"
);
}
#[test]
fn a_read_failure_reaching_only_the_csum_tree_search_is_not_confused_with_absence() {
let mut blocks = HashMap::new();
blocks.insert(
LEAF_A,
leaf(
LEAF_A,
crate::chunk::objectid::ROOT_TREE,
&[(
key(FS_TREE_OBJECTID, ROOT_ITEM_KEY, 0),
minimal_root_item(0xF00D),
)],
),
);
blocks.insert(
ROOT,
node(
ROOT,
crate::chunk::objectid::ROOT_TREE,
1,
&[
(key(FS_TREE_OBJECTID, ROOT_ITEM_KEY, 0), LEAF_A),
(
key(crate::csum::CSUM_TREE_OBJECTID, ROOT_ITEM_KEY, 0),
LEAF_B,
),
],
),
);
let read = move |logical: u64, buf: &mut [u8]| -> Result<()> {
match blocks.get(&logical) {
Some(block) => {
buf.copy_from_slice(&block[..buf.len()]);
Ok(())
}
None => Err(Error::Io(format!(
"simulated device failure reading {logical:#x}"
))),
}
};
let tree = Tree::new(geom(), &read);
let result = fs_and_csum_tree_roots(&tree, ROOT);
assert!(
result.is_err(),
"a read failure reaching leaf B while searching for the csum \
tree must propagate, not collapse into Ok((fs_root, None)) \
the way rust-fs-btrfs#114 describes"
);
}
#[test]
fn the_fs_tree_lookup_alone_never_touches_leaf_b() {
let a = leaf(
LEAF_A,
crate::chunk::objectid::ROOT_TREE,
&[(
key(FS_TREE_OBJECTID, ROOT_ITEM_KEY, 0),
minimal_root_item(0xF00D),
)],
);
let r = node(
ROOT,
crate::chunk::objectid::ROOT_TREE,
1,
&[
(key(FS_TREE_OBJECTID, ROOT_ITEM_KEY, 0), LEAF_A),
(
key(crate::csum::CSUM_TREE_OBJECTID, ROOT_ITEM_KEY, 0),
LEAF_B,
),
],
);
let blocks = HashMap::from([(LEAF_A, a), (ROOT, r)]);
let read = move |logical: u64, buf: &mut [u8]| -> Result<()> {
let block = blocks.get(&logical).unwrap_or_else(|| {
panic!("the fs-tree lookup touched {logical:#x}, expected only ROOT and LEAF_A")
});
buf.copy_from_slice(&block[..buf.len()]);
Ok(())
};
let tree = Tree::new(geom(), &read);
let found = required_root_item_target(&tree, ROOT, FS_TREE_OBJECTID).unwrap();
assert_eq!(found, 0xF00D);
}
#[test]
fn the_fixture_leaf_has_room_for_the_root_item() {
assert!(NODESIZE as usize > 2 * (root_item::BYTENR + 8) + 128);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::chunk::{block_group, Chunk, Stripe};
struct Marked(u8, u64);
impl BlockRead for Marked {
fn read_at(&self, _offset: u64, buf: &mut [u8]) -> fs_core::Result<()> {
buf.fill(self.0);
Ok(())
}
fn size_bytes(&self) -> u64 {
self.1
}
}
fn raid1_stripe0_on_devid2() -> ChunkMap {
let mut map = ChunkMap::new();
map.insert(Chunk {
logical: 0x100000,
length: 0x100000,
owner: 2,
stripe_len: 0x10000,
chunk_type: block_group::METADATA | block_group::RAID1,
io_align: 4096,
io_width: 4096,
sector_size: 4096,
num_stripes: 2,
sub_stripes: 0,
stripes: vec![
Stripe {
devid: 2,
offset: 0x200000,
dev_uuid: [0; 16],
},
Stripe {
devid: 1,
offset: 0x300000,
dev_uuid: [0; 16],
},
],
})
.expect("a two-stripe RAID1 chunk is well-formed");
map
}
#[test]
fn the_primary_read_of_a_pool_follows_the_devid_not_only_the_offset() {
let map = raid1_stripe0_on_devid2();
let logical = 0x100000;
let m0 = map.map_mirror(logical, 0).expect("mirror 0");
let m1 = map.map_mirror(logical, 1).expect("mirror 1");
assert_eq!(m0.devid, 2, "stripe 0 must be on devid 2 for this test");
assert_eq!(m1.devid, 1, "stripe 1 must be on devid 1 for this test");
let dev1: Arc<dyn BlockRead> = Arc::new(Marked(0x11, 1 << 30));
let dev2: Arc<dyn BlockRead> = Arc::new(Marked(0x22, 1 << 30));
let devices: BTreeMap<u64, Arc<dyn BlockRead>> =
[(1, dev1.clone()), (2, dev2.clone())].into_iter().collect();
let device = dev1.clone();
let mut buf = [0u8; 8];
Filesystem::read_logical_pool(&device, &devices, &map, logical, &mut buf)
.expect("the pool read must succeed");
assert_eq!(
buf, [0x22; 8],
"copy 0 lives on devid 2, so the pool read must return devid 2's bytes"
);
let mut buf = [0u8; 8];
Filesystem::read_logical_on_mirror(&device, &map, u64::MAX, 0, logical, &mut buf)
.expect("the devid-unchecked read succeeds -- that is the problem");
assert_eq!(
buf, [0x11; 8],
"the single-device form returned devid 1's bytes for a copy that is on \
devid 2, and reported success: wrong data rather than an error, which is \
why the mount path must not use it on a pool"
);
}
}