use crate::chunk::{key_type, objectid, DiskKey};
use crate::error::{Error, Result};
use crate::fs::Filesystem;
pub use crate::inode::{INODE_NODATACOW, INODE_NODATASUM};
mod extent_item {
pub const REFS: usize = 0;
pub const GENERATION: usize = 8;
}
impl Filesystem {
pub fn write_at(&self, ino: u64, offset: u64, data: &[u8]) -> Result<usize> {
if self.writable.is_none() {
return Err(Error::ReadOnly);
}
if data.is_empty() {
return Ok(0);
}
let inode = self.read_inode(ino)?;
if !inode.is_regular_file() {
return Err(Error::NotAFile);
}
if inode.flags & INODE_NODATACOW == 0 {
return Err(Error::UnsupportedFeature(format!(
"inode {ino} is copy-on-write, so overwriting it in place would leave the \
extent tree, the checksum tree and the superblock generation describing \
something that is no longer there"
)));
}
if inode.flags & INODE_NODATASUM == 0 {
return Err(Error::UnsupportedFeature(format!(
"inode {ino} is nodatacow but still checksummed, so writing it in place \
would leave every checksum item for it wrong"
)));
}
let end = offset
.checked_add(data.len() as u64)
.ok_or_else(|| Error::UnsupportedFeature("write range overflows".into()))?;
if end > inode.size {
return Err(Error::UnsupportedFeature(format!(
"inode {ino}: writing to {end} would grow the file past its {} bytes, \
which allocates",
inode.size
)));
}
let plan = self.plan_nodatacow_write(ino, offset, data.len())?;
let device = self.writable.as_ref().expect("checked above");
let mut done = 0usize;
for &(logical, len) in &plan {
Self::mirror_spans(device, &self.map, logical, len)?;
done += len;
}
debug_assert_eq!(done, data.len());
done = 0;
for (logical, len) in plan {
Self::write_logical_all_mirrors(device, &self.map, logical, &data[done..done + len])?;
done += len;
}
device.flush()?;
Ok(done)
}
pub fn can_write_in_place(&self, ino: u64) -> Result<bool> {
let inode = self.read_inode(ino)?;
if !inode.is_regular_file() {
return Ok(false);
}
if inode.flags & (INODE_NODATACOW | INODE_NODATASUM) != (INODE_NODATACOW | INODE_NODATASUM)
{
return Ok(false);
}
let last_snapshot = self.fs_tree_last_snapshot()?;
let pieces = match self.file_extents(ino) {
Ok(pieces) => pieces,
Err(Error::BadSuperblock(_)) => return Ok(false),
Err(e) => return Err(e),
};
for piece in pieces {
let Some(logical) = piece.logical else {
return Ok(false);
};
if piece.compressed {
return Ok(false);
}
let (refs, extent_len, generation) = self.extent_item(piece.extent_start)?;
if refs != 1
|| generation <= last_snapshot
|| !window_inside_extent(logical, piece.len, piece.extent_start, extent_len)
{
return Ok(false);
}
}
Ok(true)
}
fn plan_nodatacow_write(&self, ino: u64, offset: u64, len: usize) -> Result<Vec<(u64, usize)>> {
let pieces = self.file_extents(ino)?;
let last_snapshot = self.fs_tree_last_snapshot()?;
let mut plan = Vec::new();
let mut done = 0usize;
while done < len {
let pos = offset + done as u64;
let Some(piece) = pieces
.iter()
.find(|p| pos >= p.start && pos < p.start + p.len)
else {
return Err(Error::UnsupportedFeature(format!(
"inode {ino}: offset {pos} is a hole, and filling it would allocate"
)));
};
if piece.compressed {
return Err(Error::UnsupportedFeature(format!(
"inode {ino}: offset {pos} is in a compressed extent, which cannot be \
rewritten without recompressing the whole of it"
)));
}
let Some(logical) = piece.logical else {
return Err(Error::UnsupportedFeature(format!(
"inode {ino}: offset {pos} is inline or preallocated, so writing it \
changes the item rather than the blocks it points at"
)));
};
let (refs, extent_len, generation) = self.extent_item(piece.extent_start)?;
if !window_inside_extent(logical, piece.len, piece.extent_start, extent_len) {
return Err(Error::UnsupportedFeature(format!(
"inode {ino}: offset {pos} maps to [{logical}, +{}), outside the \
{extent_len}-byte extent the extent tree records at {}",
piece.len, piece.extent_start
)));
}
if refs != 1 {
return Err(Error::UnsupportedFeature(format!(
"inode {ino}: the extent at {} has {refs} references, so something \
else — most likely a snapshot — is still reading it",
piece.extent_start
)));
}
if generation <= last_snapshot {
return Err(Error::UnsupportedFeature(format!(
"inode {ino}: the extent at {} is from generation {generation}, at or \
before the snapshot taken at {last_snapshot}, so the snapshot may \
still be reading it",
piece.extent_start
)));
}
let within = pos - piece.start;
let chunk = ((piece.len - within) as usize).min(len - done);
plan.push((logical + within, chunk));
done += chunk;
}
Ok(plan)
}
pub(crate) fn extent_item(&self, bytenr: u64) -> Result<(u64, u64, u64)> {
let root = self.extent_tree_root()?;
let reader = self.pool_reader();
let tree = reader.tree();
let mut refs = None;
for item in tree.find_all(root, bytenr, key_type::EXTENT_ITEM)? {
if item.data.len() >= extent_item::GENERATION + 8 {
let data = &item.data;
let le64 =
|at: usize| u64::from_le_bytes(data[at..at + 8].try_into().expect("8 bytes"));
refs = Some((
le64(extent_item::REFS),
item.key.offset,
le64(extent_item::GENERATION),
));
break;
}
}
refs.ok_or_else(|| {
Error::UnsupportedFeature(format!(
"the extent tree holds no item for the extent at {bytenr}, so whether it \
is shared cannot be established"
))
})
}
pub(crate) fn fs_tree_last_snapshot(&self) -> Result<u64> {
use crate::fs::{root_item, FS_TREE_OBJECTID, ROOT_ITEM_KEY};
let reader = self.pool_reader();
let mut found = None;
reader
.tree()
.for_each(self.sb.root, &mut |key: &DiskKey, data: &[u8]| {
if key.objectid == FS_TREE_OBJECTID && key.key_type == ROOT_ITEM_KEY {
found = data
.get(root_item::LAST_SNAPSHOT..root_item::LAST_SNAPSHOT + 8)
.map(|b| u64::from_le_bytes(b.try_into().expect("8 bytes")));
return Ok(false);
}
Ok(true)
})?;
found.ok_or_else(|| {
Error::UnsupportedFeature(
"the root tree holds no readable ROOT_ITEM for the default subvolume, so \
whether a snapshot shares its extents cannot be established"
.into(),
)
})
}
fn extent_tree_root(&self) -> Result<u64> {
self.tree_root(objectid::EXTENT_TREE)
}
}
pub(crate) fn window_inside_extent(
logical: u64,
len: u64,
extent_start: u64,
extent_len: u64,
) -> bool {
match (
logical.checked_add(len),
extent_start.checked_add(extent_len),
) {
(Some(piece_end), Some(extent_end)) => logical >= extent_start && piece_end <= extent_end,
_ => false,
}
}