use crate::error::Result;
use crate::fs::Filesystem;
use crate::fs::ROOT_ITEM_KEY;
const ROOT_BACKREF_KEY: u8 = 144;
pub use crate::chunk::objectid::FS_TREE as FS_TREE_OBJECTID;
pub use crate::inode::FIRST_FREE_OBJECTID;
pub const LAST_FREE_OBJECTID: u64 = u64::MAX - 255;
use crate::fs::root_item;
mod root_ref {
pub const NAME_LEN: usize = 16;
pub const NAME: usize = 18;
}
const ROOT_SUBVOL_RDONLY: u64 = 1 << 0;
const ROOT_REF_KEY: u8 = 156;
pub const EMPTY_SUBVOL_DIR_OBJECTID: u64 = 2;
pub struct PathTarget {
pub tree: Option<Filesystem>,
pub inode: crate::inode::Inode,
}
impl PathTarget {
pub fn fs<'a>(&'a self, start: &'a Filesystem) -> &'a Filesystem {
self.tree.as_ref().unwrap_or(start)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Subvolume {
pub id: u64,
pub parent: u64,
pub name: Vec<u8>,
pub path: String,
pub bytenr: u64,
pub generation: u64,
pub last_snapshot: u64,
pub read_only: bool,
pub is_snapshot: bool,
}
impl Subvolume {
pub fn is_default(&self) -> bool {
self.id == FS_TREE_OBJECTID
}
}
pub fn is_subvolume_id(objectid: u64) -> bool {
objectid == FS_TREE_OBJECTID || (FIRST_FREE_OBJECTID..=LAST_FREE_OBJECTID).contains(&objectid)
}
impl Filesystem {
pub fn subvolumes(&self) -> Result<Vec<Subvolume>> {
let items = self.root_tree_items()?;
let mut names: std::collections::BTreeMap<u64, (u64, Vec<u8>)> =
std::collections::BTreeMap::new();
for (objectid, key_type, offset, data) in &items {
if *key_type != ROOT_BACKREF_KEY || !is_subvolume_id(*objectid) {
continue;
}
let Some(name) = reference_name(data) else {
continue;
};
names.insert(*objectid, (*offset, name));
}
let mut out = Vec::new();
for (objectid, key_type, offset, data) in &items {
if *key_type != ROOT_ITEM_KEY
|| !is_subvolume_id(*objectid)
|| data.len() < root_item::MIN_SIZE
{
continue;
}
let le64 =
|at: usize| u64::from_le_bytes(data[at..at + 8].try_into().expect("8 bytes"));
let (parent, name) = names.get(objectid).cloned().unwrap_or((0, Vec::new()));
out.push(Subvolume {
id: *objectid,
parent,
path: path_of(*objectid, &names),
name,
bytenr: le64(root_item::BYTENR),
generation: le64(root_item::GENERATION),
last_snapshot: le64(root_item::LAST_SNAPSHOT),
read_only: le64(root_item::FLAGS) & ROOT_SUBVOL_RDONLY != 0,
is_snapshot: *offset != 0,
});
}
out.sort_by_key(|s| s.id);
Ok(out)
}
}
fn reference_name(data: &[u8]) -> Option<Vec<u8>> {
if data.len() < root_ref::NAME {
return None;
}
let len = u16::from_le_bytes(
data[root_ref::NAME_LEN..root_ref::NAME_LEN + 2]
.try_into()
.expect("2 bytes"),
) as usize;
let end = root_ref::NAME.checked_add(len)?;
if end > data.len() {
return None;
}
Some(data[root_ref::NAME..end].to_vec())
}
fn path_of(id: u64, names: &std::collections::BTreeMap<u64, (u64, Vec<u8>)>) -> String {
if id == FS_TREE_OBJECTID {
return "/".to_string();
}
let mut parts: Vec<String> = Vec::new();
let mut at = id;
for _ in 0..names.len() + 1 {
let Some((parent, name)) = names.get(&at) else {
break;
};
parts.push(String::from_utf8_lossy(name).into_owned());
if *parent == FS_TREE_OBJECTID {
break;
}
at = *parent;
}
parts.reverse();
parts.join("/")
}
impl Filesystem {
pub fn open_subvolume(&self, id: u64) -> Result<Filesystem> {
let subvol = self
.subvolumes()?
.into_iter()
.find(|s| s.id == id)
.ok_or(crate::error::Error::NotFound)?;
self.open_subvolume_at(subvol.bytenr)
}
pub fn open_subvolume_at(&self, bytenr: u64) -> Result<Filesystem> {
self.reroot(bytenr)
}
pub fn resolve_path(&self, path: &str) -> Result<PathTarget> {
self.resolve_path_bytes(path.as_bytes())
}
pub fn resolve_path_bytes(&self, path: &[u8]) -> Result<PathTarget> {
use crate::error::Error;
let mut tree: Option<Filesystem> = None;
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 resolve_path".into(),
));
}
if !inode.is_dir() {
return Err(Error::NotADirectory);
}
if inode.ino == EMPTY_SUBVOL_DIR_OBJECTID {
return Err(Error::NotFound);
}
let here = tree.as_ref().unwrap_or(self);
let entry = here.lookup_entry(inode.ino, component)?;
if entry.is_inode() {
inode = here.read_inode(entry.ino)?;
continue;
}
match here.subvolume_behind(inode.ino, &entry)? {
Some((bytenr, dirid)) => {
let child = here.reroot(bytenr)?;
inode = child.read_inode(dirid)?;
tree = Some(child);
}
None => inode = empty_subvolume_dir(&inode),
}
}
Ok(PathTarget { tree, inode })
}
fn subvolume_behind(
&self,
dir_ino: u64,
entry: &crate::dir::DirEntry,
) -> Result<Option<(u64, u64)>> {
use crate::error::Error;
let reader = self.pool_reader();
let root_tree = reader.tree();
let root_item_of = |id: u64| -> Result<Option<Vec<u8>>> {
Ok(root_tree
.find_all(self.sb.root, id, ROOT_ITEM_KEY)?
.into_iter()
.next()
.map(|item| item.data))
};
let Some(child) = root_item_of(entry.ino)? else {
return Ok(None);
};
if child.len() < root_item::MIN_SIZE {
return Err(Error::BadSuperblock(format!(
"subvolume {}: ROOT_ITEM is {} bytes",
entry.ino,
child.len()
)));
}
let le64 = |data: &[u8], at: usize| {
u64::from_le_bytes(data[at..at + 8].try_into().expect("8 bytes"))
};
for backref in root_tree.find_all(self.sb.root, entry.ino, ROOT_BACKREF_KEY)? {
let parent = backref.key.offset;
let same_tree = root_item_of(parent)?.is_some_and(|data| {
data.len() >= root_item::MIN_SIZE
&& le64(&data, root_item::BYTENR) == self.fs_tree_root
});
if !same_tree {
continue;
}
let backed = root_tree
.search(
self.sb.root,
&crate::chunk::DiskKey {
objectid: parent,
key_type: ROOT_REF_KEY,
offset: entry.ino,
},
)?
.is_some_and(|item| {
item.data.len() >= 8
&& le64(&item.data, 0) == dir_ino
&& reference_name(&item.data).as_deref() == Some(&entry.name[..])
});
if backed {
return Ok(Some((
le64(&child, root_item::BYTENR),
le64(&child, root_item::ROOT_DIRID),
)));
}
}
Ok(None)
}
}
fn empty_subvolume_dir(dir: &crate::inode::Inode) -> crate::inode::Inode {
crate::inode::Inode {
ino: EMPTY_SUBVOL_DIR_OBJECTID,
size: 0,
nbytes: 0,
nlink: 1,
uid: 0,
gid: 0,
mode: 0o040755,
rdev: 0,
flags: 0,
atime: dir.ctime,
mtime: dir.ctime,
ctime: dir.ctime,
otime: dir.ctime,
..dir.clone()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn internal_trees_are_not_subvolumes() {
assert!(is_subvolume_id(FS_TREE_OBJECTID), "the default subvolume");
assert!(is_subvolume_id(256), "the first created one");
assert!(is_subvolume_id(1000));
assert!(!is_subvolume_id(1), "the root tree");
assert!(!is_subvolume_id(2), "the extent tree");
assert!(!is_subvolume_id(255), "below the free range");
assert!(
!is_subvolume_id(u64::MAX - 8),
"a negatively numbered internal tree is not a subvolume"
);
assert!(!is_subvolume_id(u64::MAX));
}
#[test]
fn a_reference_name_is_read_from_its_declared_length() {
let mut item = vec![0u8; 18];
item[16..18].copy_from_slice(&3u16.to_le_bytes());
item.extend_from_slice(b"sub");
assert_eq!(reference_name(&item).as_deref(), Some(&b"sub"[..]));
let mut lying = vec![0u8; 18];
lying[16..18].copy_from_slice(&99u16.to_le_bytes());
lying.extend_from_slice(b"sub");
assert_eq!(reference_name(&lying), None);
assert_eq!(
reference_name(&[0u8; 4]),
None,
"too short to hold a header"
);
}
#[test]
fn a_path_is_the_chain_of_parents() {
let mut names = std::collections::BTreeMap::new();
names.insert(256u64, (5u64, b"sub".to_vec()));
names.insert(257u64, (256u64, b"inner".to_vec()));
assert_eq!(path_of(5, &names), "/");
assert_eq!(path_of(256, &names), "sub");
assert_eq!(path_of(257, &names), "sub/inner");
}
#[test]
fn a_cycle_in_the_references_terminates() {
let mut names = std::collections::BTreeMap::new();
names.insert(256u64, (257u64, b"a".to_vec()));
names.insert(257u64, (256u64, b"b".to_vec()));
let path = path_of(256, &names);
assert!(!path.is_empty());
}
}