use crate::{
CacheConfig, Entry, FileKind, Stat,
cache::{ExtentMapCache, InodeCache, LruTreeBlockCache},
dir, read,
stat::to_system_time,
xattr,
};
use btrfs_disk::{
items::{
DeviceItem, DirItem, FileExtentBody, InodeExtref, InodeItem, InodeRef,
RootItem, RootItemFlags, RootRef, Timespec as DiskTimespec,
},
reader::{BlockReader, Traversal, filesystem_open, tree_walk},
superblock::Superblock,
tree::{KeyType, TreeBlock},
};
use btrfs_stream::{StreamCommand, StreamWriter, Timespec as StreamTimespec};
use std::{
collections::BTreeMap,
io, mem,
sync::{Arc, Mutex, MutexGuard},
time::SystemTime,
};
use uuid::Uuid;
const FS_TREE_OBJECTID: u64 = 5;
const ROOT_DIR_OBJECTID: u64 = 256;
const LAST_FREE_OBJECTID: u64 = u64::MAX - 256;
const SEND_STREAM_VERSION: u32 = 1;
const SEND_WRITE_CHUNK_BYTES: usize = 48 * 1024;
fn is_subvolume_id(id: u64) -> bool {
id == FS_TREE_OBJECTID
|| (ROOT_DIR_OBJECTID..=LAST_FREE_OBJECTID).contains(&id)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct SubvolId(pub u64);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Inode {
pub subvol: SubvolId,
pub ino: u64,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct SubvolInfo {
pub id: SubvolId,
pub parent: Option<SubvolId>,
pub name: Vec<u8>,
pub dirid: u64,
pub readonly: bool,
pub ctime: SystemTime,
pub otime: SystemTime,
pub generation: u64,
pub ctransid: u64,
pub otransid: u64,
pub uuid: Uuid,
pub parent_uuid: Uuid,
pub received_uuid: Uuid,
}
#[derive(Debug, Clone, Copy)]
pub struct SearchFilter {
pub tree_id: u64,
pub min_objectid: u64,
pub max_objectid: u64,
pub min_type: u32,
pub max_type: u32,
pub min_offset: u64,
pub max_offset: u64,
pub min_transid: u64,
pub max_transid: u64,
pub max_items: u32,
}
#[derive(Debug, Clone)]
pub struct SearchItem {
pub transid: u64,
pub objectid: u64,
pub item_type: u32,
pub offset: u64,
pub data: Vec<u8>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SeekHoleData {
Data,
Hole,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct StatFs {
pub blocks: u64,
pub bfree: u64,
pub bavail: u64,
pub bsize: u32,
pub namelen: u32,
pub frsize: u32,
}
pub struct Filesystem<R: io::Read + io::Seek + Send + 'static> {
inner: Arc<Inner<R>>,
}
impl<R: io::Read + io::Seek + Send + 'static> Clone for Filesystem<R> {
fn clone(&self) -> Self {
Self {
inner: Arc::clone(&self.inner),
}
}
}
struct Inner<R: io::Read + io::Seek + Send + 'static> {
superblock: Superblock,
tree_roots: BTreeMap<u64, (u64, u64)>,
default_subvol: SubvolId,
blksize: u32,
reader: Mutex<BlockReader<R>>,
tree_block_cache: Arc<LruTreeBlockCache>,
inode_cache: InodeCache,
extent_map_cache: ExtentMapCache,
}
impl<R: io::Read + io::Seek + Send + 'static> Filesystem<R> {
pub fn open(reader: R) -> io::Result<Self> {
Self::open_inner(
reader,
SubvolId(FS_TREE_OBJECTID),
CacheConfig::default(),
)
}
pub fn open_subvol(reader: R, subvol: SubvolId) -> io::Result<Self> {
Self::open_inner(reader, subvol, CacheConfig::default())
}
pub fn open_with_caches(
reader: R,
cache_config: CacheConfig,
) -> io::Result<Self> {
Self::open_inner(reader, SubvolId(FS_TREE_OBJECTID), cache_config)
}
pub fn open_subvol_with_caches(
reader: R,
subvol: SubvolId,
cache_config: CacheConfig,
) -> io::Result<Self> {
Self::open_inner(reader, subvol, cache_config)
}
fn open_inner(
reader: R,
default_subvol: SubvolId,
cache_config: CacheConfig,
) -> io::Result<Self> {
if !is_subvolume_id(default_subvol.0) {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"{} is not a valid subvolume id (must be 5 or in \
[256, u64::MAX - 256])",
default_subvol.0,
),
));
}
let mut fs = filesystem_open(reader)
.map_err(|e| io::Error::other(e.to_string()))?;
let blksize = fs.superblock.sectorsize;
if !fs.tree_roots.contains_key(&FS_TREE_OBJECTID) {
return Err(io::Error::other(
"default FS tree (objectid 5) not found in root tree",
));
}
if !fs.tree_roots.contains_key(&default_subvol.0) {
return Err(io::Error::new(
io::ErrorKind::NotFound,
format!("subvolume {} not found", default_subvol.0),
));
}
let tree_block_cache =
Arc::new(LruTreeBlockCache::new(cache_config.tree_blocks));
fs.reader.set_cache(Some(tree_block_cache.clone()
as Arc<dyn btrfs_disk::reader::TreeBlockCache>));
Ok(Self {
inner: Arc::new(Inner {
superblock: fs.superblock,
tree_roots: fs.tree_roots,
default_subvol,
blksize,
reader: Mutex::new(fs.reader),
tree_block_cache,
inode_cache: InodeCache::new(cache_config.inodes),
extent_map_cache: ExtentMapCache::new(cache_config.extent_maps),
}),
})
}
#[must_use]
pub fn tree_block_cache_stats(&self) -> crate::CacheStats {
self.inner.tree_block_cache.stats()
}
#[must_use]
pub fn root(&self) -> Inode {
Inode {
subvol: self.inner.default_subvol,
ino: ROOT_DIR_OBJECTID,
}
}
#[must_use]
pub fn default_subvol(&self) -> SubvolId {
self.inner.default_subvol
}
pub async fn list_subvolumes(&self) -> io::Result<Vec<SubvolInfo>> {
let this = self.clone();
spawn_blocking(move || this.list_subvolumes_blocking()).await
}
pub async fn get_subvol_info(
&self,
id: SubvolId,
) -> io::Result<Option<SubvolInfo>> {
Ok(self
.list_subvolumes()
.await?
.into_iter()
.find(|s| s.id == id))
}
#[must_use]
pub fn superblock(&self) -> &Superblock {
&self.inner.superblock
}
#[must_use]
pub fn dev_info(&self, devid: u64) -> Option<DeviceItem> {
if self.inner.superblock.dev_item.devid == devid {
Some(self.inner.superblock.dev_item.clone())
} else {
None
}
}
pub async fn tree_search(
&self,
filter: SearchFilter,
max_buf_size: usize,
) -> io::Result<Vec<SearchItem>> {
let this = self.clone();
spawn_blocking(move || this.tree_search_blocking(filter, max_buf_size))
.await
}
pub async fn ino_lookup(
&self,
subvol: SubvolId,
objectid: u64,
) -> io::Result<Option<Vec<u8>>> {
let this = self.clone();
spawn_blocking(move || this.ino_lookup_blocking(subvol, objectid)).await
}
pub async fn ino_paths(
&self,
subvol: SubvolId,
objectid: u64,
) -> io::Result<Vec<Vec<u8>>> {
let this = self.clone();
spawn_blocking(move || this.ino_paths_blocking(subvol, objectid)).await
}
#[must_use]
pub fn blksize(&self) -> u32 {
self.inner.blksize
}
pub fn forget(&self, ino: Inode) {
self.inner.inode_cache.invalidate(ino);
self.inner.extent_map_cache.invalidate(ino);
}
pub async fn lookup(
&self,
parent: Inode,
name: &[u8],
) -> io::Result<Option<(Inode, InodeItem)>> {
let this = self.clone();
let name = name.to_vec();
spawn_blocking(move || this.lookup_blocking(parent, &name)).await
}
pub async fn read_inode_item(
&self,
ino: Inode,
) -> io::Result<Option<InodeItem>> {
let this = self.clone();
spawn_blocking(move || this.read_inode_item_blocking(ino)).await
}
pub async fn getattr(&self, ino: Inode) -> io::Result<Option<Stat>> {
let this = self.clone();
spawn_blocking(move || this.getattr_blocking(ino)).await
}
pub async fn readdir(
&self,
dir_ino: Inode,
offset: u64,
) -> io::Result<Vec<Entry>> {
let this = self.clone();
spawn_blocking(move || this.readdir_blocking(dir_ino, offset)).await
}
pub async fn readdirplus(
&self,
dir_ino: Inode,
offset: u64,
) -> io::Result<Vec<(Entry, Stat)>> {
let this = self.clone();
spawn_blocking(move || this.readdirplus_blocking(dir_ino, offset)).await
}
pub async fn readlink(&self, ino: Inode) -> io::Result<Option<Vec<u8>>> {
let this = self.clone();
spawn_blocking(move || this.readlink_blocking(ino)).await
}
pub async fn read(
&self,
ino: Inode,
offset: u64,
size: u32,
) -> io::Result<Vec<u8>> {
let this = self.clone();
spawn_blocking(move || this.read_blocking(ino, offset, size)).await
}
pub async fn resolve_subvol_path(
&self,
path: &str,
) -> io::Result<Option<SubvolId>> {
let trimmed = path.trim_matches('/');
if trimmed.is_empty() {
return Ok(Some(SubvolId(FS_TREE_OBJECTID)));
}
let subvols = self.list_subvolumes().await?;
let by_id: BTreeMap<SubvolId, &SubvolInfo> =
subvols.iter().map(|s| (s.id, s)).collect();
let target = trimmed.as_bytes();
for s in &subvols {
if subvol_full_path(&by_id, s.id) == target {
return Ok(Some(s.id));
}
}
Ok(None)
}
pub async fn send<W: io::Write + Send + 'static>(
&self,
snapshot: SubvolId,
output: W,
) -> io::Result<W> {
let this = self.clone();
spawn_blocking(move || this.send_blocking(snapshot, output)).await
}
pub async fn seek_hole_data(
&self,
ino: Inode,
offset: u64,
whence: SeekHoleData,
) -> io::Result<u64> {
let this = self.clone();
spawn_blocking(move || {
this.seek_hole_data_blocking(ino, offset, whence)
})
.await
}
pub async fn xattr_list(&self, ino: Inode) -> io::Result<Vec<Vec<u8>>> {
let this = self.clone();
spawn_blocking(move || this.xattr_list_blocking(ino)).await
}
pub async fn xattr_get(
&self,
ino: Inode,
name: &[u8],
) -> io::Result<Option<Vec<u8>>> {
let this = self.clone();
let name = name.to_vec();
spawn_blocking(move || this.xattr_get_blocking(ino, &name)).await
}
#[must_use]
pub fn statfs(&self) -> StatFs {
let sb = &self.inner.superblock;
let bsize = u64::from(sb.sectorsize);
let blocks = sb.total_bytes / bsize;
let bfree = sb.total_bytes.saturating_sub(sb.bytes_used) / bsize;
StatFs {
blocks,
bfree,
bavail: bfree,
bsize: sb.sectorsize,
namelen: 255,
frsize: sb.sectorsize,
}
}
fn lookup_blocking(
&self,
parent: Inode,
name: &[u8],
) -> io::Result<Option<(Inode, InodeItem)>> {
let parent_tree = self.tree_root_for(parent.subvol)?;
let mut reader = self.lock_reader();
let Some(entry) =
lookup_in_dir(&mut reader, parent_tree, parent.ino, name)?
else {
return Ok(None);
};
let child = if entry.location.key_type == KeyType::RootItem {
Inode {
subvol: SubvolId(entry.location.objectid),
ino: ROOT_DIR_OBJECTID,
}
} else {
Inode {
subvol: parent.subvol,
ino: entry.location.objectid,
}
};
let item = if let Some(cached) = self.inner.inode_cache.get(child) {
(*cached).clone()
} else {
let child_tree = self.tree_root_for(child.subvol)?;
let Some(item) = read_inode(&mut reader, child_tree, child.ino)?
else {
return Ok(None);
};
self.inner.inode_cache.put(child, Arc::new(item.clone()));
item
};
Ok(Some((child, item)))
}
fn read_inode_item_blocking(
&self,
ino: Inode,
) -> io::Result<Option<InodeItem>> {
if let Some(cached) = self.inner.inode_cache.get(ino) {
return Ok(Some((*cached).clone()));
}
let tree_root = self.tree_root_for(ino.subvol)?;
let mut reader = self.lock_reader();
let Some(item) = read_inode(&mut reader, tree_root, ino.ino)? else {
return Ok(None);
};
self.inner.inode_cache.put(ino, Arc::new(item.clone()));
Ok(Some(item))
}
fn getattr_blocking(&self, ino: Inode) -> io::Result<Option<Stat>> {
Ok(self
.read_inode_item_blocking(ino)?
.map(|item| Stat::from_inode(ino, &item, self.inner.blksize)))
}
fn readdir_blocking(
&self,
dir_ino: Inode,
offset: u64,
) -> io::Result<Vec<Entry>> {
let tree_root = self.tree_root_for(dir_ino.subvol)?;
let mut entries: Vec<Entry> = Vec::new();
let mut reader = self.lock_reader();
if offset == 0 {
entries.push(Entry {
ino: dir_ino,
kind: FileKind::Directory,
name: b".".to_vec(),
offset: 1,
});
}
if offset <= 1 {
let parent = if dir_ino.ino == ROOT_DIR_OBJECTID
&& dir_ino.subvol.0 != FS_TREE_OBJECTID
{
find_root_backref_parent(
&mut reader,
self.inner.superblock.root,
dir_ino.subvol.0,
)?
.unwrap_or(dir_ino)
} else {
let parent_oid =
find_parent_oid(&mut reader, tree_root, dir_ino.ino)?;
Inode {
subvol: dir_ino.subvol,
ino: parent_oid,
}
};
entries.push(Entry {
ino: parent,
kind: FileKind::Directory,
name: b"..".to_vec(),
offset: 2,
});
}
let cursor = offset.max(2);
let mut dir_entries: Vec<Entry> = Vec::new();
for_each_item(&mut reader, tree_root, |key, data| {
if key.objectid != dir_ino.ino || key.key_type != KeyType::DirIndex
{
return;
}
if key.offset < cursor {
return;
}
for item in DirItem::parse_all(data) {
let entry = dir::Entry::from_dir_item(
dir_ino.subvol,
&item,
key.offset + 1,
);
dir_entries.push(entry);
}
})?;
dir_entries.sort_by_key(|e| e.offset);
entries.extend(dir_entries);
Ok(entries)
}
fn readdirplus_blocking(
&self,
dir_ino: Inode,
offset: u64,
) -> io::Result<Vec<(Entry, Stat)>> {
let entries = self.readdir_blocking(dir_ino, offset)?;
let blksize = self.inner.blksize;
let mut out = Vec::with_capacity(entries.len());
for entry in entries {
if let Some(item) = self.read_inode_item_blocking(entry.ino)? {
let stat = Stat::from_inode(entry.ino, &item, blksize);
out.push((entry, stat));
}
}
Ok(out)
}
fn readlink_blocking(&self, ino: Inode) -> io::Result<Option<Vec<u8>>> {
let Some(item) = self.read_inode_item_blocking(ino)? else {
return Ok(None);
};
let tree_root = self.tree_root_for(ino.subvol)?;
let blksize = self.inner.blksize;
let mut reader = self.lock_reader();
let target =
read::read_symlink(&mut reader, tree_root, ino.ino, blksize)?;
#[allow(clippy::cast_possible_truncation)]
Ok(target.map(|mut t| {
t.truncate(item.size as usize);
t
}))
}
fn read_blocking(
&self,
ino: Inode,
offset: u64,
size: u32,
) -> io::Result<Vec<u8>> {
let Some(item) = self.read_inode_item_blocking(ino)? else {
return Err(io::Error::new(
io::ErrorKind::NotFound,
format!("inode {} not found", ino.ino),
));
};
let tree_root = self.tree_root_for(ino.subvol)?;
let blksize = self.inner.blksize;
let extent_map = self.extent_map_for(ino, tree_root)?;
let mut reader = self.lock_reader();
read::read_file_with_map(
&mut reader,
&extent_map.records,
item.size,
offset,
size,
blksize,
)
}
fn send_blocking<W: io::Write>(
&self,
snapshot: SubvolId,
output: W,
) -> io::Result<W> {
let info = self
.list_subvolumes_blocking()?
.into_iter()
.find(|s| s.id == snapshot)
.ok_or_else(|| {
io::Error::new(
io::ErrorKind::NotFound,
format!("subvolume {} not found", snapshot.0),
)
})?;
let mut writer = StreamWriter::new(output, SEND_STREAM_VERSION)?;
let subvol_path = if info.name.is_empty() {
format!("subvol-{}", info.id.0)
} else {
String::from_utf8_lossy(&info.name).into_owned()
};
writer.write_command(&StreamCommand::Subvol {
path: subvol_path,
uuid: info.uuid,
ctransid: info.ctransid,
})?;
let root = Inode {
subvol: snapshot,
ino: ROOT_DIR_OBJECTID,
};
let mut seen: BTreeMap<u64, String> = BTreeMap::new();
seen.insert(ROOT_DIR_OBJECTID, String::new());
self.send_dir_recursive(&mut writer, root, "", &mut seen)?;
writer.write_command(&StreamCommand::End)?;
writer.finish()
}
fn send_dir_recursive<W: io::Write>(
&self,
writer: &mut StreamWriter<W>,
dir: Inode,
dir_path: &str,
seen: &mut BTreeMap<u64, String>,
) -> io::Result<()> {
let entries = self.readdir_blocking(dir, 1)?;
let mut subdirs: Vec<(Inode, String)> = Vec::new();
for entry in entries {
if entry.name == b"." || entry.name == b".." {
continue;
}
if entry.ino.subvol != dir.subvol {
continue;
}
let entry_name = String::from_utf8_lossy(&entry.name).into_owned();
let entry_path = if dir_path.is_empty() {
entry_name
} else {
format!("{dir_path}/{entry_name}")
};
if let Some(first_path) = seen.get(&entry.ino.ino) {
writer.write_command(&StreamCommand::Link {
path: entry_path.clone(),
target: first_path.clone(),
})?;
continue;
}
let item =
self.read_inode_item_blocking(entry.ino)?.ok_or_else(|| {
io::Error::new(
io::ErrorKind::NotFound,
format!("inode {} item missing", entry.ino.ino),
)
})?;
seen.insert(entry.ino.ino, entry_path.clone());
self.send_create_command(writer, &entry, &entry_path, &item)?;
self.send_xattrs(writer, entry.ino, &entry_path)?;
if entry.kind == FileKind::RegularFile {
self.send_file_data(writer, entry.ino, &entry_path, item.size)?;
writer.write_command(&StreamCommand::Truncate {
path: entry_path.clone(),
size: item.size,
})?;
}
send_metadata(writer, &entry_path, &item)?;
if entry.kind == FileKind::Directory {
subdirs.push((entry.ino, entry_path));
}
}
for (subdir_ino, subdir_path) in subdirs {
self.send_dir_recursive(writer, subdir_ino, &subdir_path, seen)?;
}
Ok(())
}
fn send_create_command<W: io::Write>(
&self,
writer: &mut StreamWriter<W>,
entry: &Entry,
path: &str,
item: &InodeItem,
) -> io::Result<()> {
let cmd = match entry.kind {
FileKind::RegularFile => {
StreamCommand::Mkfile { path: path.into() }
}
FileKind::Directory => StreamCommand::Mkdir { path: path.into() },
FileKind::Symlink => {
let target =
self.readlink_blocking(entry.ino)?.ok_or_else(|| {
io::Error::new(
io::ErrorKind::NotFound,
format!("symlink {} target missing", entry.ino.ino),
)
})?;
StreamCommand::Symlink {
path: path.into(),
target: String::from_utf8_lossy(&target).into_owned(),
}
}
FileKind::NamedPipe => StreamCommand::Mkfifo { path: path.into() },
FileKind::Socket => StreamCommand::Mksock { path: path.into() },
FileKind::BlockDevice | FileKind::CharDevice => {
StreamCommand::Mknod {
path: path.into(),
mode: u64::from(item.mode),
rdev: item.rdev,
}
}
};
writer.write_command(&cmd)
}
fn send_xattrs<W: io::Write>(
&self,
writer: &mut StreamWriter<W>,
ino: Inode,
path: &str,
) -> io::Result<()> {
for name in self.xattr_list_blocking(ino)? {
let Some(data) = self.xattr_get_blocking(ino, &name)? else {
continue;
};
writer.write_command(&StreamCommand::SetXattr {
path: path.into(),
name: String::from_utf8_lossy(&name).into_owned(),
data,
})?;
}
Ok(())
}
fn send_file_data<W: io::Write>(
&self,
writer: &mut StreamWriter<W>,
ino: Inode,
path: &str,
size: u64,
) -> io::Result<()> {
let mut offset = 0u64;
while offset < size {
let remaining = size - offset;
#[allow(clippy::cast_possible_truncation)]
let chunk = remaining.min(SEND_WRITE_CHUNK_BYTES as u64) as u32;
let data = self.read_blocking(ino, offset, chunk)?;
if data.is_empty() {
break;
}
writer.write_command(&StreamCommand::Write {
path: path.into(),
offset,
data,
})?;
offset += u64::from(chunk);
}
Ok(())
}
fn seek_hole_data_blocking(
&self,
ino: Inode,
offset: u64,
whence: SeekHoleData,
) -> io::Result<u64> {
let item = self.read_inode_item_blocking(ino)?.ok_or_else(|| {
io::Error::new(
io::ErrorKind::NotFound,
format!("inode {} not found", ino.ino),
)
})?;
let file_size = item.size;
if offset >= file_size {
return Err(io::Error::from_raw_os_error(libc::ENXIO));
}
let tree_root = self.tree_root_for(ino.subvol)?;
let extent_map = self.extent_map_for(ino, tree_root)?;
let want_hole = matches!(whence, SeekHoleData::Hole);
let mut cursor = 0u64;
for r in &extent_map.records {
if r.file_pos > cursor {
let hole_end = r.file_pos.min(file_size);
if hole_end > offset && want_hole {
return Ok(offset.max(cursor));
}
cursor = hole_end;
if cursor >= file_size {
break;
}
}
let body_len = match &r.item.body {
FileExtentBody::Inline { .. } => r.item.ram_bytes,
FileExtentBody::Regular { num_bytes, .. } => *num_bytes,
};
let r_start = r.file_pos.max(cursor);
let r_end = (r.file_pos + body_len).min(file_size);
if r_end <= r_start {
continue;
}
let r_is_hole = matches!(
&r.item.body,
FileExtentBody::Regular { disk_bytenr: 0, .. },
);
if r_end > offset && r_is_hole == want_hole {
return Ok(offset.max(r_start));
}
cursor = r_end;
if cursor >= file_size {
break;
}
}
if want_hole {
if cursor < file_size && cursor > offset {
Ok(cursor)
} else if offset < file_size {
Ok(file_size)
} else {
Err(io::Error::from_raw_os_error(libc::ENXIO))
}
} else {
Err(io::Error::from_raw_os_error(libc::ENXIO))
}
}
fn extent_map_for(
&self,
ino: Inode,
tree_root: u64,
) -> io::Result<Arc<crate::cache::ExtentMap>> {
if let Some(cached) = self.inner.extent_map_cache.get(ino) {
return Ok(cached);
}
let mut reader = self.lock_reader();
let records = read::collect_extents(&mut reader, tree_root, ino.ino)?;
drop(reader);
let map = Arc::new(crate::cache::ExtentMap { records });
self.inner.extent_map_cache.put(ino, Arc::clone(&map));
Ok(map)
}
fn xattr_list_blocking(&self, ino: Inode) -> io::Result<Vec<Vec<u8>>> {
let tree_root = self.tree_root_for(ino.subvol)?;
let mut reader = self.lock_reader();
xattr::list_xattrs(&mut reader, tree_root, ino.ino)
}
fn xattr_get_blocking(
&self,
ino: Inode,
name: &[u8],
) -> io::Result<Option<Vec<u8>>> {
let tree_root = self.tree_root_for(ino.subvol)?;
let mut reader = self.lock_reader();
xattr::get_xattr(&mut reader, tree_root, ino.ino, name)
}
fn tree_search_blocking(
&self,
filter: SearchFilter,
max_buf_size: usize,
) -> io::Result<Vec<SearchItem>> {
const HEADER_SIZE: usize = 32;
let tree_root = if filter.tree_id == 1 {
self.inner.superblock.root
} else {
self.tree_root_for(SubvolId(filter.tree_id))?
};
let min = (filter.min_objectid, filter.min_type, filter.min_offset);
let max = (filter.max_objectid, filter.max_type, filter.max_offset);
let mut results: Vec<SearchItem> = Vec::new();
let mut buf_used: usize = 0;
let mut reader = self.lock_reader();
tree_walk(&mut reader, tree_root, Traversal::Dfs, &mut |block| {
if results.len() >= filter.max_items as usize {
return;
}
let TreeBlock::Leaf {
items,
data,
header,
} = block
else {
return;
};
let leaf_transid = header.generation;
if leaf_transid < filter.min_transid
|| leaf_transid > filter.max_transid
{
return;
}
let hdr_size = mem::size_of::<btrfs_disk::raw::btrfs_header>();
for item in items {
if results.len() >= filter.max_items as usize {
return;
}
let key = &item.key;
let item_type = u32::from(key.key_type.to_raw());
let compound = (key.objectid, item_type, key.offset);
if compound < min || compound > max {
continue;
}
let start = hdr_size + item.offset as usize;
let end = start + item.size as usize;
if end > data.len() {
continue;
}
let payload = &data[start..end];
let next_used = buf_used + HEADER_SIZE + payload.len();
if next_used > max_buf_size {
return;
}
results.push(SearchItem {
transid: leaf_transid,
objectid: key.objectid,
item_type,
offset: key.offset,
data: payload.to_vec(),
});
buf_used = next_used;
}
})?;
Ok(results)
}
fn ino_lookup_blocking(
&self,
subvol: SubvolId,
objectid: u64,
) -> io::Result<Option<Vec<u8>>> {
if objectid == ROOT_DIR_OBJECTID {
return Ok(Some(Vec::new()));
}
let tree_root = self.tree_root_for(subvol)?;
let mut reader = self.lock_reader();
let mut components: Vec<Vec<u8>> = Vec::new();
let mut current = objectid;
for _ in 0..4096 {
if current == ROOT_DIR_OBJECTID {
components.reverse();
return Ok(Some(join_path(&components)));
}
let mut next_parent: Option<u64> = None;
let mut name: Option<Vec<u8>> = None;
for_each_item(&mut reader, tree_root, |key, data| {
if next_parent.is_some() {
return;
}
if key.objectid == current && key.key_type == KeyType::InodeRef
{
if let Some(iref) =
InodeRef::parse_all(data).into_iter().next()
{
next_parent = Some(key.offset);
name = Some(iref.name);
}
}
})?;
match (next_parent, name) {
(Some(p), Some(n)) => {
components.push(n);
current = p;
}
_ => return Ok(None),
}
}
Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("INODE_REF chain for objectid {objectid} too deep"),
))
}
fn ino_paths_blocking(
&self,
subvol: SubvolId,
objectid: u64,
) -> io::Result<Vec<Vec<u8>>> {
if objectid == ROOT_DIR_OBJECTID {
return Ok(vec![Vec::new()]);
}
let tree_root = self.tree_root_for(subvol)?;
let mut refs: Vec<(u64, Vec<u8>)> = Vec::new();
let mut reader = self.lock_reader();
for_each_item(&mut reader, tree_root, |key, data| {
if key.objectid != objectid {
return;
}
match key.key_type {
KeyType::InodeRef => {
for iref in InodeRef::parse_all(data) {
refs.push((key.offset, iref.name));
}
}
KeyType::InodeExtref => {
for eref in InodeExtref::parse_all(data) {
refs.push((eref.parent, eref.name));
}
}
_ => {}
}
})?;
drop(reader);
let mut paths = Vec::with_capacity(refs.len());
for (parent, name) in refs {
let Some(parent_path) = self.ino_lookup_blocking(subvol, parent)?
else {
continue;
};
let mut p = parent_path;
if !p.is_empty() {
p.push(b'/');
}
p.extend_from_slice(&name);
paths.push(p);
}
Ok(paths)
}
fn list_subvolumes_blocking(&self) -> io::Result<Vec<SubvolInfo>> {
let root_tree = self.inner.superblock.root;
let mut roots: BTreeMap<u64, RootItem> = BTreeMap::new();
let mut backrefs: BTreeMap<u64, (u64, RootRef)> = BTreeMap::new();
let mut reader = self.lock_reader();
for_each_item(&mut reader, root_tree, |key, data| {
match key.key_type {
KeyType::RootItem if is_subvolume_id(key.objectid) => {
if let Some(item) = RootItem::parse(data) {
roots.entry(key.objectid).or_insert(item);
}
}
KeyType::RootBackref => {
if let Some(rr) = RootRef::parse(data) {
backrefs
.entry(key.objectid)
.or_insert((key.offset, rr));
}
}
_ => {}
}
})?;
drop(reader);
let mut out = Vec::with_capacity(roots.len());
for (id, item) in roots {
let (parent, name, dirid) = match backrefs.get(&id) {
Some((parent_id, rr)) => {
(Some(SubvolId(*parent_id)), rr.name.clone(), rr.dirid)
}
None => (None, Vec::new(), 0),
};
out.push(SubvolInfo {
id: SubvolId(id),
parent,
name,
dirid,
readonly: item.flags.contains(RootItemFlags::RDONLY),
ctime: to_system_time(&item.ctime),
otime: to_system_time(&item.otime),
generation: item.generation,
ctransid: item.ctransid,
otransid: item.otransid,
uuid: item.uuid,
parent_uuid: item.parent_uuid,
received_uuid: item.received_uuid,
});
}
Ok(out)
}
fn lock_reader(&self) -> MutexGuard<'_, BlockReader<R>> {
self.inner.reader.lock().unwrap()
}
fn tree_root_for(&self, subvol: SubvolId) -> io::Result<u64> {
if !is_subvolume_id(subvol.0) {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("{} is not a valid subvolume id", subvol.0),
));
}
self.inner
.tree_roots
.get(&subvol.0)
.map(|(logical, _)| *logical)
.ok_or_else(|| {
io::Error::new(
io::ErrorKind::NotFound,
format!("subvolume {} not found", subvol.0),
)
})
}
}
async fn spawn_blocking<F, T>(f: F) -> io::Result<T>
where
F: FnOnce() -> io::Result<T> + Send + 'static,
T: Send + 'static,
{
tokio::task::spawn_blocking(f)
.await
.map_err(|e| io::Error::other(format!("blocking task failed: {e}")))?
}
fn for_each_item<R, F>(
reader: &mut BlockReader<R>,
tree_root: u64,
mut visitor: F,
) -> io::Result<()>
where
R: io::Read + io::Seek,
F: FnMut(&btrfs_disk::tree::DiskKey, &[u8]),
{
tree_walk(reader, tree_root, Traversal::Dfs, &mut |block| {
if let TreeBlock::Leaf { items, data, .. } = block {
let header_size = mem::size_of::<btrfs_disk::raw::btrfs_header>();
for item in items {
let start = header_size + item.offset as usize;
let end = start + item.size as usize;
if end <= data.len() {
visitor(&item.key, &data[start..end]);
}
}
}
})
}
fn read_inode<R: io::Read + io::Seek>(
reader: &mut BlockReader<R>,
tree_root: u64,
objectid: u64,
) -> io::Result<Option<InodeItem>> {
let mut found = None;
for_each_item(reader, tree_root, |key, data| {
if found.is_some() {
return;
}
if key.objectid == objectid && key.key_type == KeyType::InodeItem {
found = InodeItem::parse(data);
}
})?;
Ok(found)
}
fn lookup_in_dir<R: io::Read + io::Seek>(
reader: &mut BlockReader<R>,
tree_root: u64,
parent_objectid: u64,
name: &[u8],
) -> io::Result<Option<DirItem>> {
let mut found = None;
for_each_item(reader, tree_root, |key, data| {
if found.is_some() {
return;
}
if key.objectid != parent_objectid || key.key_type != KeyType::DirItem {
return;
}
for item in DirItem::parse_all(data) {
if item.name == name {
found = Some(item);
return;
}
}
})?;
Ok(found)
}
fn subvol_full_path(
by_id: &BTreeMap<SubvolId, &SubvolInfo>,
id: SubvolId,
) -> Vec<u8> {
let mut components: Vec<Vec<u8>> = Vec::new();
let mut current = id;
while let Some(info) = by_id.get(¤t) {
if !info.name.is_empty() {
components.push(info.name.clone());
}
match info.parent {
Some(parent) => current = parent,
None => break,
}
}
components.reverse();
let mut out: Vec<u8> = Vec::new();
for (i, c) in components.iter().enumerate() {
if i > 0 {
out.push(b'/');
}
out.extend_from_slice(c);
}
out
}
fn to_stream_timespec(t: &DiskTimespec) -> StreamTimespec {
StreamTimespec {
sec: t.sec,
nsec: t.nsec,
}
}
fn send_metadata<W: io::Write>(
writer: &mut StreamWriter<W>,
path: &str,
item: &InodeItem,
) -> io::Result<()> {
writer.write_command(&StreamCommand::Chown {
path: path.into(),
uid: u64::from(item.uid),
gid: u64::from(item.gid),
})?;
writer.write_command(&StreamCommand::Chmod {
path: path.into(),
mode: u64::from(item.mode & 0o7777),
})?;
writer.write_command(&StreamCommand::Utimes {
path: path.into(),
atime: to_stream_timespec(&item.atime),
mtime: to_stream_timespec(&item.mtime),
ctime: to_stream_timespec(&item.ctime),
})?;
Ok(())
}
fn join_path(components: &[Vec<u8>]) -> Vec<u8> {
let total = components.iter().map(Vec::len).sum::<usize>()
+ components.len().saturating_sub(1);
let mut out: Vec<u8> = Vec::with_capacity(total);
for (i, c) in components.iter().enumerate() {
if i > 0 {
out.push(b'/');
}
out.extend_from_slice(c);
}
out
}
fn find_parent_oid<R: io::Read + io::Seek>(
reader: &mut BlockReader<R>,
tree_root: u64,
oid: u64,
) -> io::Result<u64> {
let mut parent = oid;
for_each_item(reader, tree_root, |key, _data| {
if parent != oid {
return;
}
if key.objectid == oid && key.key_type == KeyType::InodeRef {
parent = key.offset;
}
})?;
Ok(parent)
}
fn find_root_backref_parent<R: io::Read + io::Seek>(
reader: &mut BlockReader<R>,
root_tree_logical: u64,
child_subvol: u64,
) -> io::Result<Option<Inode>> {
let mut found = None;
for_each_item(reader, root_tree_logical, |key, data| {
if found.is_some() {
return;
}
if key.objectid == child_subvol && key.key_type == KeyType::RootBackref
{
if let Some(rr) = RootRef::parse(data) {
found = Some(Inode {
subvol: SubvolId(key.offset),
ino: rr.dirid,
});
}
}
})?;
Ok(found)
}