use std::collections::BTreeMap;
use crate::{EntryMode, LooseObjects, ObjectId, Tree, TreeEntry, TreeError, TreeValue};
#[derive(Debug)]
pub struct TreeEditor<'a> {
objects: &'a LooseObjects,
entries: BTreeMap<Vec<u8>, TreeValue>,
}
impl<'a> TreeEditor<'a> {
pub fn new(objects: &'a LooseObjects) -> Self {
Self {
objects,
entries: BTreeMap::new(),
}
}
pub fn set(&mut self, path: impl AsRef<[u8]>, blob: ObjectId) -> Result<(), TreeWriteError> {
self.set_entry(
path,
TreeValue {
mode: EntryMode::Blob,
id: blob,
},
)
}
pub fn set_entry(
&mut self,
path: impl AsRef<[u8]>,
value: TreeValue,
) -> Result<(), TreeWriteError> {
let path = path.as_ref();
validate_path(path)?;
value
.id
.require_format(self.objects.object_format())
.map_err(TreeError::from)?;
self.check_ancestors(path)?;
self.remove_subtree(path);
let mut ancestor = split_path(path).0;
while !ancestor.is_empty() {
self.entries.remove(ancestor);
ancestor = split_path(ancestor).0;
}
self.entries.insert(path.to_vec(), value);
Ok(())
}
pub fn remove(&mut self, path: impl AsRef<[u8]>) -> Result<(), TreeWriteError> {
let path = path.as_ref();
validate_path(path)?;
self.check_ancestors(path)?;
self.remove_subtree(path);
Ok(())
}
fn check_ancestors(&self, path: &[u8]) -> Result<(), TreeWriteError> {
let mut ancestor = split_path(path).0;
while !ancestor.is_empty() {
if self
.entries
.get(ancestor)
.is_some_and(|value| value.mode == EntryMode::Tree)
{
return Err(TreeWriteError::StoredSubtree {
path: ancestor.to_vec(),
});
}
ancestor = split_path(ancestor).0;
}
Ok(())
}
fn remove_subtree(&mut self, path: &[u8]) {
self.entries.remove(path);
let prefix = [path, b"/"].concat();
let descendants: Vec<_> = self
.entries
.range(prefix.clone()..)
.take_while(|(key, _)| key.starts_with(&prefix))
.map(|(key, _)| key.clone())
.collect();
for key in descendants {
self.entries.remove(&key);
}
}
pub fn write(&self) -> Result<ObjectId, TreeWriteError> {
let mut directories: BTreeMap<Vec<u8>, Vec<TreeEntry>> = BTreeMap::new();
directories.insert(Vec::new(), Vec::new());
for (path, value) in &self.entries {
let (parent, name) = split_path(path);
directories
.entry(parent.to_vec())
.or_default()
.push(TreeEntry {
name: name.to_vec(),
mode: value.mode,
id: value.id,
});
let mut ancestor = parent;
while !ancestor.is_empty() {
ancestor = split_path(ancestor).0;
directories.entry(ancestor.to_vec()).or_default();
}
}
while directories.len() > 1 {
let Some((path, entries)) = directories.pop_last() else {
break;
};
let tree = Tree::new(self.objects.object_format(), entries)?;
let id = self.objects.write_tree(&tree)?;
let (parent, name) = split_path(&path);
directories
.entry(parent.to_vec())
.or_default()
.push(TreeEntry {
name: name.to_vec(),
mode: EntryMode::Tree,
id,
});
}
let tree = Tree::new(
self.objects.object_format(),
directories.remove(b"".as_slice()).unwrap_or_default(),
)?;
Ok(self.objects.write_tree(&tree)?)
}
}
fn split_path(path: &[u8]) -> (&[u8], &[u8]) {
match path.iter().rposition(|&byte| byte == b'/') {
Some(index) => {
let (parent, suffix) = path.split_at(index);
(parent, suffix.strip_prefix(b"/").unwrap_or_default())
}
None => (&[], path),
}
}
fn validate_path(path: &[u8]) -> Result<(), TreeWriteError> {
for name in path.split(|&byte| byte == b'/') {
if name.is_empty() || name.contains(&0) || name == b"." || name == b".." {
return Err(TreeError::InvalidName.into());
}
}
Ok(())
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum TreeWriteError {
#[error(transparent)]
Tree(#[from] TreeError),
#[error(transparent)]
Storage(#[from] crate::loose::Error),
#[error("path traverses a stored subtree")]
StoredSubtree {
path: Vec<u8>,
},
}