use crate::entry::Entry;
use crate::error::{IncludeError, Result};
use crate::options::EntryOptions;
use std::collections::{HashMap, HashSet};
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Debug, Clone)]
pub struct RemovedEntry {
pub entry: Entry,
pub path: String,
}
#[derive(Debug, Default, Clone)]
pub struct TreeDiff {
pub created: Vec<Entry>,
pub updated: Vec<Entry>,
pub redefined: Vec<Entry>,
pub moved: Vec<Entry>,
pub removed: Vec<RemovedEntry>,
}
impl TreeDiff {
pub fn is_empty(&self) -> bool {
self.created.is_empty()
&& self.updated.is_empty()
&& self.redefined.is_empty()
&& self.moved.is_empty()
&& self.removed.is_empty()
}
}
pub struct EntryTree {
root: Entry,
mutation: std::sync::Mutex<()>,
}
impl Default for EntryTree {
fn default() -> Self {
Self::new()
}
}
impl EntryTree {
pub fn new() -> Self {
Self {
root: Entry::new_root(),
mutation: std::sync::Mutex::new(()),
}
}
pub fn root(&self) -> &Entry {
&self.root
}
pub fn top_level(&self) -> Vec<Entry> {
self.root.children()
}
pub fn entries(&self) -> Vec<Entry> {
let mut out = Vec::new();
fn walk(entry: &Entry, out: &mut Vec<Entry>) {
for child in entry.children() {
out.push(child.clone());
walk(&child, out);
}
}
walk(&self.root, &mut out);
out
}
pub fn resolve(&self, id: &str) -> Option<Entry> {
let mut current = self.root.clone();
for part in id.split(':') {
current = current
.children()
.into_iter()
.find(|child| child.id() == part)?;
}
Some(current)
}
pub fn serialize(&self) -> Vec<EntryOptions> {
fn to_options(entry: &Entry) -> EntryOptions {
let mut options = entry.options();
options.group = entry.children().iter().map(to_options).collect();
options
}
self.root.children().iter().map(to_options).collect()
}
pub fn create(
&self,
options: EntryOptions,
parent: Option<&Entry>,
position: Option<usize>,
) -> Result<Entry> {
let _guard = crate::lock(&self.mutation);
let parent = parent.unwrap_or(&self.root);
self.assert_owned(parent)?;
if options.name.is_empty() {
return Err(IncludeError::InvalidName);
}
let reserved: HashSet<String> = self.ids();
validate_subtree(
std::slice::from_ref(&options),
&reserved,
&mut HashSet::new(),
)?;
let mut options = options;
let id = match options.id.take() {
Some(id) => id,
None => generate_id(&reserved, &HashMap::new()),
};
let group = std::mem::take(&mut options.group);
options.id = Some(id.clone());
let entry = Entry::new(id, options);
let children = sync_children(
&entry,
group,
&mut HashMap::new(),
&reserved,
&mut TreeDiff::default(),
);
entry.set_children(children);
insert_child(parent, entry.clone(), position);
Ok(entry)
}
pub fn remove(&self, id: &str) -> Result<Entry> {
let _guard = crate::lock(&self.mutation);
let entry = self
.resolve(id)
.ok_or_else(|| IncludeError::EntryNotFound { id: id.to_owned() })?;
let parent = entry
.parent()
.ok_or_else(|| IncludeError::EntryNotFound { id: id.to_owned() })?;
detach_child(&parent, &entry);
Ok(entry)
}
pub fn update_entry(
&self,
id: &str,
options: EntryOptions,
new_parent: Option<&Entry>,
position: Option<usize>,
) -> Result<Entry> {
let _guard = crate::lock(&self.mutation);
let entry = self
.resolve(id)
.ok_or_else(|| IncludeError::EntryNotFound { id: id.to_owned() })?;
let old_parent = entry
.parent()
.ok_or_else(|| IncludeError::EntryNotFound { id: id.to_owned() })?;
let parent = match new_parent {
Some(parent) => {
self.assert_owned(parent)?;
if entry.contains(parent) {
return Err(IncludeError::Cycle);
}
parent.clone()
}
None => old_parent.clone(),
};
if options.name.is_empty() {
return Err(IncludeError::InvalidName);
}
let subtree = descendants(&entry);
let mut pool: HashMap<String, Entry> = subtree
.iter()
.map(|child| (child.id().to_string(), child.clone()))
.collect();
let mut reserved = self.ids();
reserved.remove(entry.id());
for key in pool.keys() {
reserved.remove(key);
}
validate_subtree(
std::slice::from_ref(&options),
&reserved,
&mut HashSet::new(),
)?;
let mut options = options;
options.id = Some(entry.id().to_owned());
let group = std::mem::take(&mut options.group);
entry.set_options(options);
detach_child(&old_parent, &entry);
let children = sync_children(
&entry,
group,
&mut pool,
&reserved,
&mut TreeDiff::default(),
);
entry.set_children(children);
insert_child(&parent, entry.clone(), position);
Ok(entry)
}
pub fn update(&self, entries: Vec<EntryOptions>) -> Result<TreeDiff> {
let _guard = crate::lock(&self.mutation);
validate_subtree(&entries, &HashSet::new(), &mut HashSet::new())?;
let paths: HashMap<String, String> = self
.entries()
.iter()
.map(|entry| (entry.id().to_string(), entry.path()))
.collect();
let mut pool: HashMap<String, Entry> = self
.entries()
.into_iter()
.map(|entry| (entry.id().to_string(), entry))
.collect();
let reserved = HashSet::new();
let mut diff = TreeDiff::default();
let children = sync_children(&self.root, entries, &mut pool, &reserved, &mut diff);
self.root.set_children(children);
diff.removed = pool
.into_values()
.map(|entry| RemovedEntry {
path: paths.get(entry.id()).cloned().unwrap_or_default(),
entry,
})
.collect();
Ok(diff)
}
fn ids(&self) -> HashSet<String> {
self.entries().iter().map(|e| e.id().to_string()).collect()
}
fn assert_owned(&self, candidate: &Entry) -> Result<()> {
let mut current = candidate.clone();
loop {
if Entry::ptr_eq(¤t, &self.root) {
return Ok(());
}
match current.parent() {
Some(parent) => current = parent,
None => return Err(IncludeError::NotInTree),
}
}
}
}
fn sync_children(
parent: &Entry,
options: Vec<EntryOptions>,
pool: &mut HashMap<String, Entry>,
reserved: &HashSet<String>,
diff: &mut TreeDiff,
) -> Vec<Entry> {
let mut result = Vec::with_capacity(options.len());
for options in options {
let mut options = options;
let id = match options.id.take() {
Some(id) => id,
None => generate_id(reserved, pool),
};
options.id = Some(id.clone());
let group = std::mem::take(&mut options.group);
let entry = match pool.remove(&id) {
Some(existing) => {
if existing.options() != options {
let structural = existing.options().name != options.name
|| existing.options().inject != options.inject
|| existing.options().disabled != options.disabled;
existing.set_options(options);
if structural {
diff.redefined.push(existing.clone());
} else {
diff.updated.push(existing.clone());
}
}
let moved = existing
.parent()
.is_none_or(|old| !Entry::ptr_eq(&old, parent));
if moved {
diff.moved.push(existing.clone());
}
existing
}
None => {
let created = Entry::new(id, options);
diff.created.push(created.clone());
created
}
};
let children = sync_children(&entry, group, pool, reserved, diff);
entry.set_children(children);
result.push(entry);
}
result
}
fn descendants(entry: &Entry) -> Vec<Entry> {
let mut out = Vec::new();
fn walk(entry: &Entry, out: &mut Vec<Entry>) {
for child in entry.children() {
out.push(child.clone());
walk(&child, out);
}
}
walk(entry, &mut out);
out
}
fn insert_child(parent: &Entry, child: Entry, position: Option<usize>) {
let mut siblings = parent.children();
let index = position.unwrap_or(siblings.len()).min(siblings.len());
siblings.insert(index, child);
parent.set_children(siblings);
}
fn detach_child(parent: &Entry, child: &Entry) {
let kept: Vec<Entry> = parent
.children()
.into_iter()
.filter(|kept| !Entry::ptr_eq(kept, child))
.collect();
parent.set_children(kept);
}
fn validate_id(id: &str) -> Result<()> {
if id.is_empty() || id.contains(':') {
return Err(IncludeError::InvalidId { id: id.to_owned() });
}
Ok(())
}
fn validate_subtree(
entries: &[EntryOptions],
reserved: &HashSet<String>,
seen: &mut HashSet<String>,
) -> Result<()> {
for options in entries {
if options.name.is_empty() {
return Err(IncludeError::InvalidName);
}
if let Some(id) = options.id.as_deref() {
validate_id(id)?;
if reserved.contains(id) {
return Err(IncludeError::DuplicateId { id: id.to_owned() });
}
if !seen.insert(id.to_owned()) {
return Err(IncludeError::DuplicateId { id: id.to_owned() });
}
}
validate_subtree(&options.group, reserved, seen)?;
}
Ok(())
}
fn generate_id(reserved: &HashSet<String>, pool: &HashMap<String, Entry>) -> String {
loop {
let candidate = random_base36_6();
if !reserved.contains(&candidate) && !pool.contains_key(&candidate) {
return candidate;
}
}
}
fn random_base36_6() -> String {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|elapsed| elapsed.as_nanos() as u64)
.unwrap_or(0);
let count = COUNTER.fetch_add(1, Ordering::Relaxed);
let mut z = nanos ^ count.wrapping_mul(0x9E37_79B9_7F4A_7C15);
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^= z >> 31;
const ALPHABET: &[u8; 36] = b"0123456789abcdefghijklmnopqrstuvwxyz";
let mut value = z % 2_176_782_336; let mut out = [0u8; 6];
for slot in out.iter_mut().rev() {
*slot = ALPHABET[(value % 36) as usize];
value /= 36;
}
String::from_utf8_lossy(&out).into_owned()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn generated_ids_are_six_base36_chars() {
for _ in 0..100 {
let id = random_base36_6();
assert_eq!(id.len(), 6, "{id}");
assert!(
id.bytes()
.all(|b| b.is_ascii_digit() || b.is_ascii_lowercase())
);
}
}
#[test]
fn generated_ids_avoid_collisions() {
let first = random_base36_6();
let reserved: HashSet<String> = [first.clone()].into_iter().collect();
assert_ne!(generate_id(&reserved, &HashMap::new()), first);
}
}