use std::collections::BTreeSet;
use std::sync::Arc;
use crate::chunk::{objectid, DiskKey};
use crate::error::{Error, Result};
use crate::fs::{Filesystem, EXTENT_DATA_KEY};
use crate::inode::{Inode, INODE_ITEM_KEY, INODE_NODATACOW, INODE_NODATASUM};
use crate::super_write::Commit;
use crate::transaction::{DataMove, DataWrite};
use crate::write::window_inside_extent;
const PLAN_ROUNDS: usize = 64;
#[derive(Debug, Clone, Copy)]
struct Target {
start: u64,
len: u64,
extent_start: u64,
extent_len: u64,
extent_offset: u64,
}
impl Filesystem {
pub fn write(&mut 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 self.write_at(ino, offset, data);
}
self.write_cow(&inode, offset, data)
}
fn write_cow(&mut self, inode: &Inode, offset: u64, data: &[u8]) -> Result<usize> {
let ino = inode.ino;
if inode.flags & INODE_NODATASUM == 0 {
return Err(Error::UnsupportedFeature(format!(
"inode {ino} is copy-on-write and checksummed, and writing it means writing \
checksum items for its new extents, which is not implemented"
)));
}
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 \
adds an extent rather than replacing one",
inode.size
)));
}
let targets = self.plan_cow_targets(ino, offset, end)?;
self.refuse_checksummed_extents(ino, &targets)?;
let lens: Vec<u64> = targets.iter().map(|t| t.len).collect();
let news = self.find_data_extents(&lens)?;
let mut contents: Vec<(u64, Vec<u8>)> = Vec::with_capacity(targets.len());
for (t, &at) in targets.iter().zip(&news) {
let size = usize::try_from(t.len)
.map_err(|_| Error::UnsupportedFeature(format!("an extent of {} bytes", t.len)))?;
let mut buf = vec![0u8; size];
self.read_at(ino, t.start, &mut buf)?;
let from = offset.max(t.start);
let to = end.min(t.start + t.len);
buf[(from - t.start) as usize..(to - t.start) as usize]
.copy_from_slice(&data[(from - offset) as usize..(to - offset) as usize]);
contents.push((at, buf));
}
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default();
let write = DataWrite {
root: objectid::FS_TREE,
ino,
moves: targets
.iter()
.zip(&news)
.map(|(t, &new)| DataMove {
old: t.extent_start,
old_len: t.extent_len,
old_ref_offset: t.start - t.extent_offset,
new,
len: t.len,
file_offset: t.start,
})
.collect(),
time: (now.as_secs(), now.subsec_nanos()),
};
let dirty: Vec<u64> = {
let reader = self.pool_reader();
let tree = reader.tree();
let mut leaves = BTreeSet::new();
let keys = std::iter::once((INODE_ITEM_KEY, 0))
.chain(targets.iter().map(|t| (EXTENT_DATA_KEY, t.start)));
for (key_type, key_offset) in keys {
let key = DiskKey {
objectid: ino,
key_type,
offset: key_offset,
};
leaves.insert(tree.descend(self.fs_tree_root, &key)?.header.bytenr);
}
leaves.into_iter().collect()
};
let generation = self.sb.generation.checked_add(1).ok_or_else(|| {
Error::UnsupportedFeature("the generation counter is exhausted".into())
})?;
let plan = self.plan_transaction_closed_with(&dirty, &write, PLAN_ROUNDS)?;
let blocks = self.render_plan_with(&plan, &write, generation)?;
let root = self.planned_root(&plan).ok_or_else(|| {
Error::UnsupportedFeature("the write's plan does not move the root tree".into())
})?;
let delta = plan.usage_delta(u64::from(self.sb.nodesize))
+ write
.moves
.iter()
.map(|m| i128::from(m.len) - i128::from(m.old_len))
.sum::<i128>();
let bytes_used = match delta {
0 => None,
d => Some(
u64::try_from(i128::from(self.sb.bytes_used) + d).map_err(|_| {
Error::UnsupportedFeature(format!(
"the superblock's {} bytes used cannot move by {d}",
self.sb.bytes_used
))
})?,
),
};
let device = Arc::clone(self.writable.as_ref().expect("checked by write"));
for (at, buf) in &contents {
Self::mirror_spans(&device, &self.map, *at, buf.len())?;
}
for (at, buf) in &contents {
Self::write_logical_all_mirrors(&device, &self.map, *at, buf)?;
}
self.commit(
&blocks,
&Commit {
generation,
root,
bytes_used,
invalidate_free_space_tree: false,
..Default::default()
},
)?;
*self = Filesystem::mount_rw(device)?;
Ok(data.len())
}
fn plan_cow_targets(&self, ino: u64, offset: u64, end: u64) -> Result<Vec<Target>> {
let pieces = self.file_extents(ino)?;
let last_snapshot = self.fs_tree_last_snapshot()?;
let mut out = Vec::new();
let mut pos = offset;
while pos < end {
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 adds an extent \
rather than replacing one"
)));
};
if piece.compressed {
return Err(Error::UnsupportedFeature(format!(
"inode {ino}: offset {pos} is in a compressed extent, and replacing one \
is not implemented"
)));
}
let Some(logical) = piece.logical else {
return Err(Error::UnsupportedFeature(format!(
"inode {ino}: offset {pos} is inline or preallocated, and writing it \
changes the item's kind, which is not implemented"
)));
};
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 a snapshot or \
another file still reads it, and releasing one reference of several \
is not implemented",
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
)));
}
out.push(Target {
start: piece.start,
len: piece.len,
extent_start: piece.extent_start,
extent_len,
extent_offset: logical - piece.extent_start,
});
pos = piece.start + piece.len;
}
Ok(out)
}
fn refuse_checksummed_extents(&self, ino: u64, targets: &[Target]) -> Result<()> {
let Ok(root) = self.tree_root(crate::csum::CSUM_TREE_OBJECTID) else {
return Ok(());
};
let reader = self.pool_reader();
let tree = reader.tree();
for t in targets {
let found = crate::csum::digests_for_range(
&tree,
root,
self.sb.csum_type.digest_len(),
u64::from(self.sb.sectorsize),
t.extent_start,
t.extent_len,
)?;
if !found.is_empty() {
return Err(Error::UnsupportedFeature(format!(
"inode {ino} is nodatasum, yet the checksum tree holds {} checksums for its \
extent at {}; releasing the extent would leave them describing nothing",
found.len(),
t.extent_start
)));
}
}
Ok(())
}
}