Skip to main content

cordis_include/
tree.rs

1//! The entry tree: id scheme, structural edits, and whole-tree diffs.
2
3use crate::entry::Entry;
4use crate::error::{IncludeError, Result};
5use crate::options::EntryOptions;
6use std::collections::{HashMap, HashSet};
7use std::time::{SystemTime, UNIX_EPOCH};
8
9/// What changed in one [`EntryTree::update`] pass.
10///
11/// The loader consumes this to start/stop/patch fibers without touching
12/// entries whose id, options, and position are unchanged.
13#[derive(Debug, Default, Clone)]
14pub struct TreeDiff {
15    /// Entries that appeared in the new data (subtree roots first).
16    pub created: Vec<Entry>,
17    /// Entries still present whose options changed.
18    pub updated: Vec<Entry>,
19    /// Entries that moved to a different parent.
20    pub moved: Vec<Entry>,
21    /// Entries whose ids vanished from the new data (whole subtrees).
22    pub removed: Vec<Entry>,
23}
24
25impl TreeDiff {
26    /// Whether nothing changed.
27    pub fn is_empty(&self) -> bool {
28        self.created.is_empty()
29            && self.updated.is_empty()
30            && self.moved.is_empty()
31            && self.removed.is_empty()
32    }
33}
34
35/// An in-memory tree of [`Entry`]s mirroring one config file.
36///
37/// Entries are addressed by id; nested entries use composite ids
38/// (`outer:inner`, see [`Entry::path`]). Entries without an explicit id get
39/// a random 6-character base36 id that is persisted on the next write-back.
40///
41/// Structural edits are serialized through an internal lock; individual
42/// reads (children, parent walks) take per-entry locks. `EntryTree` is not
43/// bound to any file — pairing it with a [`crate::LoaderFile`] is the
44/// caller's (usually the loader's) job.
45pub struct EntryTree {
46    root: Entry,
47    mutation: std::sync::Mutex<()>,
48}
49
50impl Default for EntryTree {
51    fn default() -> Self {
52        Self::new()
53    }
54}
55
56impl EntryTree {
57    /// Create an empty tree.
58    pub fn new() -> Self {
59        Self {
60            root: Entry::new_root(),
61            mutation: std::sync::Mutex::new(()),
62        }
63    }
64
65    /// The synthetic root holding the top-level entries. It is addressed by
66    /// the empty id and never returned by [`EntryTree::resolve`].
67    pub fn root(&self) -> &Entry {
68        &self.root
69    }
70
71    /// Top-level entries in file order.
72    pub fn top_level(&self) -> Vec<Entry> {
73        self.root.children()
74    }
75
76    /// All entries in depth-first order, parents before children.
77    pub fn entries(&self) -> Vec<Entry> {
78        let mut out = Vec::new();
79        fn walk(entry: &Entry, out: &mut Vec<Entry>) {
80            for child in entry.children() {
81                out.push(child.clone());
82                walk(&child, out);
83            }
84        }
85        walk(&self.root, &mut out);
86        out
87    }
88
89    /// Look an entry up by (possibly composite) id, e.g. `group1:child2`.
90    pub fn resolve(&self, id: &str) -> Option<Entry> {
91        let mut current = self.root.clone();
92        for part in id.split(':') {
93            current = current
94                .children()
95                .into_iter()
96                .find(|child| child.id() == part)?;
97        }
98        Some(current)
99    }
100
101    /// Serialize the whole tree back to options, generated ids included.
102    pub fn serialize(&self) -> Vec<EntryOptions> {
103        fn to_options(entry: &Entry) -> EntryOptions {
104            let mut options = entry.options();
105            options.group = entry.children().iter().map(to_options).collect();
106            options
107        }
108        self.root.children().iter().map(to_options).collect()
109    }
110
111    /// Create an entry below `parent` (the tree root when `None`) at
112    /// `position` (appended when `None`). `options.group` seeds the child
113    /// list for group entries. Returns the created entry.
114    pub fn create(
115        &self,
116        options: EntryOptions,
117        parent: Option<&Entry>,
118        position: Option<usize>,
119    ) -> Result<Entry> {
120        let _guard = crate::lock(&self.mutation);
121        let parent = parent.unwrap_or(&self.root);
122        self.assert_owned(parent)?;
123        if options.name.is_empty() {
124            return Err(IncludeError::InvalidName);
125        }
126        let reserved: HashSet<String> = self.ids();
127        validate_subtree(
128            std::slice::from_ref(&options),
129            &reserved,
130            &mut HashSet::new(),
131        )?;
132
133        let mut options = options;
134        let id = match options.id.take() {
135            Some(id) => id,
136            None => generate_id(&reserved, &HashMap::new()),
137        };
138        let group = std::mem::take(&mut options.group);
139        options.id = Some(id.clone());
140        let entry = Entry::new(id, options);
141        let children = sync_children(
142            &entry,
143            group,
144            &mut HashMap::new(),
145            &reserved,
146            &mut TreeDiff::default(),
147        );
148        entry.set_children(children);
149        insert_child(parent, entry.clone(), position);
150        Ok(entry)
151    }
152
153    /// Detach the entry with the given id and return it with its subtree
154    /// intact, so the loader can still stop the fibers inside it.
155    pub fn remove(&self, id: &str) -> Result<Entry> {
156        let _guard = crate::lock(&self.mutation);
157        let entry = self
158            .resolve(id)
159            .ok_or_else(|| IncludeError::EntryNotFound { id: id.to_owned() })?;
160        let parent = entry
161            .parent()
162            .ok_or_else(|| IncludeError::EntryNotFound { id: id.to_owned() })?;
163        detach_child(&parent, &entry);
164        Ok(entry)
165    }
166
167    /// Update one entry's options and optionally move it below
168    /// `new_parent` (appended unless `position` is given). The entry id is
169    /// identity and survives the update; `options.id` is ignored.
170    /// `options.group` re-syncs the entry's children, reusing existing
171    /// subtree entries whose ids match.
172    pub fn update_entry(
173        &self,
174        id: &str,
175        options: EntryOptions,
176        new_parent: Option<&Entry>,
177        position: Option<usize>,
178    ) -> Result<Entry> {
179        let _guard = crate::lock(&self.mutation);
180        let entry = self
181            .resolve(id)
182            .ok_or_else(|| IncludeError::EntryNotFound { id: id.to_owned() })?;
183        let old_parent = entry
184            .parent()
185            .ok_or_else(|| IncludeError::EntryNotFound { id: id.to_owned() })?;
186        let parent = match new_parent {
187            Some(parent) => {
188                self.assert_owned(parent)?;
189                if entry.contains(parent) {
190                    return Err(IncludeError::Cycle);
191                }
192                parent.clone()
193            }
194            None => old_parent.clone(),
195        };
196        if options.name.is_empty() {
197            return Err(IncludeError::InvalidName);
198        }
199
200        // Existing subtree entries stay reusable by id; everything else in
201        // the tree is reserved.
202        let subtree = descendants(&entry);
203        let mut pool: HashMap<String, Entry> = subtree
204            .iter()
205            .map(|child| (child.id().to_string(), child.clone()))
206            .collect();
207        let mut reserved = self.ids();
208        // The entry's own id is identity, not a duplicate; reuse and
209        // subtree ids are already excluded above.
210        reserved.remove(entry.id());
211        for key in pool.keys() {
212            reserved.remove(key);
213        }
214        validate_subtree(
215            std::slice::from_ref(&options),
216            &reserved,
217            &mut HashSet::new(),
218        )?;
219
220        let mut options = options;
221        options.id = Some(entry.id().to_owned());
222        let group = std::mem::take(&mut options.group);
223        entry.set_options(options);
224
225        // Detach first so a cross-group move never leaves the entry in two
226        // sibling lists at once.
227        detach_child(&old_parent, &entry);
228        let children = sync_children(
229            &entry,
230            group,
231            &mut pool,
232            &reserved,
233            &mut TreeDiff::default(),
234        );
235        entry.set_children(children);
236        insert_child(&parent, entry.clone(), position);
237        Ok(entry)
238    }
239
240    /// Reload the whole tree from new options, reusing existing entries
241    /// wherever ids match — including entries that moved between groups —
242    /// and returning what changed.
243    ///
244    /// Entries in the new data without an id cannot be matched and are
245    /// always created fresh; persist generated ids by writing
246    /// [`EntryTree::serialize`] back to the file.
247    pub fn update(&self, entries: Vec<EntryOptions>) -> Result<TreeDiff> {
248        let _guard = crate::lock(&self.mutation);
249        // Existing ids are reusable here, so nothing is reserved; only
250        // duplicates within the incoming data are rejected.
251        validate_subtree(&entries, &HashSet::new(), &mut HashSet::new())?;
252
253        // Index every existing entry by id so matches work across groups.
254        let mut pool: HashMap<String, Entry> = self
255            .entries()
256            .into_iter()
257            .map(|entry| (entry.id().to_string(), entry))
258            .collect();
259        let reserved = HashSet::new();
260        let mut diff = TreeDiff::default();
261        let children = sync_children(&self.root, entries, &mut pool, &reserved, &mut diff);
262        self.root.set_children(children);
263        diff.removed = pool.into_values().collect();
264        Ok(diff)
265    }
266
267    /// All entry ids currently in the tree.
268    fn ids(&self) -> HashSet<String> {
269        self.entries().iter().map(|e| e.id().to_string()).collect()
270    }
271
272    /// Fail unless `candidate` is the root of, or lives inside, this tree.
273    fn assert_owned(&self, candidate: &Entry) -> Result<()> {
274        let mut current = candidate.clone();
275        loop {
276            if Entry::ptr_eq(&current, &self.root) {
277                return Ok(());
278            }
279            match current.parent() {
280                Some(parent) => current = parent,
281                None => return Err(IncludeError::NotInTree),
282            }
283        }
284    }
285}
286
287/// Build the new child list for `parent` from `options`, taking reusable
288/// entries out of `pool` (leftovers become `removed`) and reporting
289/// mutations through `diff`. Infallible: callers pre-validate ids.
290fn sync_children(
291    parent: &Entry,
292    options: Vec<EntryOptions>,
293    pool: &mut HashMap<String, Entry>,
294    reserved: &HashSet<String>,
295    diff: &mut TreeDiff,
296) -> Vec<Entry> {
297    let mut result = Vec::with_capacity(options.len());
298    for options in options {
299        let mut options = options;
300        let id = match options.id.take() {
301            Some(id) => id,
302            None => generate_id(reserved, pool),
303        };
304        options.id = Some(id.clone());
305        let group = std::mem::take(&mut options.group);
306        let entry = match pool.remove(&id) {
307            Some(existing) => {
308                if existing.options() != options {
309                    existing.set_options(options);
310                    diff.updated.push(existing.clone());
311                }
312                let moved = existing
313                    .parent()
314                    .is_none_or(|old| !Entry::ptr_eq(&old, parent));
315                if moved {
316                    diff.moved.push(existing.clone());
317                }
318                existing
319            }
320            None => {
321                let created = Entry::new(id, options);
322                diff.created.push(created.clone());
323                created
324            }
325        };
326        let children = sync_children(&entry, group, pool, reserved, diff);
327        entry.set_children(children);
328        result.push(entry);
329    }
330    result
331}
332
333/// All entries strictly below `entry`, depth-first.
334fn descendants(entry: &Entry) -> Vec<Entry> {
335    let mut out = Vec::new();
336    fn walk(entry: &Entry, out: &mut Vec<Entry>) {
337        for child in entry.children() {
338            out.push(child.clone());
339            walk(&child, out);
340        }
341    }
342    walk(entry, &mut out);
343    out
344}
345
346/// Insert `child` into `parent`'s child list at `position` (append when
347/// `None`, clamped to the ends).
348fn insert_child(parent: &Entry, child: Entry, position: Option<usize>) {
349    let mut siblings = parent.children();
350    let index = position.unwrap_or(siblings.len()).min(siblings.len());
351    siblings.insert(index, child);
352    parent.set_children(siblings);
353}
354
355/// Remove `child` from `parent`'s child list, leaving the child's own
356/// subtree intact and clearing its parent link.
357fn detach_child(parent: &Entry, child: &Entry) {
358    let kept: Vec<Entry> = parent
359        .children()
360        .into_iter()
361        .filter(|kept| !Entry::ptr_eq(kept, child))
362        .collect();
363    parent.set_children(kept);
364}
365
366/// Reject empty ids and the `:` path separator.
367fn validate_id(id: &str) -> Result<()> {
368    if id.is_empty() || id.contains(':') {
369        return Err(IncludeError::InvalidId { id: id.to_owned() });
370    }
371    Ok(())
372}
373
374/// Pre-validate an incoming options tree before any mutation: non-empty
375/// names, well-formed ids, no duplicate explicit ids within the data, and
376/// no collision with `reserved` (ids outside the reusable pool).
377fn validate_subtree(
378    entries: &[EntryOptions],
379    reserved: &HashSet<String>,
380    seen: &mut HashSet<String>,
381) -> Result<()> {
382    for options in entries {
383        if options.name.is_empty() {
384            return Err(IncludeError::InvalidName);
385        }
386        if let Some(id) = options.id.as_deref() {
387            validate_id(id)?;
388            if reserved.contains(id) {
389                return Err(IncludeError::DuplicateId { id: id.to_owned() });
390            }
391            if !seen.insert(id.to_owned()) {
392                return Err(IncludeError::DuplicateId { id: id.to_owned() });
393            }
394        }
395        validate_subtree(&options.group, reserved, seen)?;
396    }
397    Ok(())
398}
399
400/// Generate a random 6-character base36 id avoiding `reserved` ids and
401/// anything still pooled.
402fn generate_id(reserved: &HashSet<String>, pool: &HashMap<String, Entry>) -> String {
403    loop {
404        let candidate = random_base36_6();
405        if !reserved.contains(&candidate) && !pool.contains_key(&candidate) {
406            return candidate;
407        }
408    }
409}
410
411/// Six base36 characters from a time/counter-seeded splitmix64 stream.
412/// Uniqueness matters, unpredictability does not.
413fn random_base36_6() -> String {
414    use std::sync::atomic::{AtomicU64, Ordering};
415    static COUNTER: AtomicU64 = AtomicU64::new(0);
416
417    let nanos = SystemTime::now()
418        .duration_since(UNIX_EPOCH)
419        .map(|elapsed| elapsed.as_nanos() as u64)
420        .unwrap_or(0);
421    let count = COUNTER.fetch_add(1, Ordering::Relaxed);
422    let mut z = nanos ^ count.wrapping_mul(0x9E37_79B9_7F4A_7C15);
423    z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
424    z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
425    z ^= z >> 31;
426
427    const ALPHABET: &[u8; 36] = b"0123456789abcdefghijklmnopqrstuvwxyz";
428    let mut value = z % 2_176_782_336; // 36^6
429    let mut out = [0u8; 6];
430    for slot in out.iter_mut().rev() {
431        *slot = ALPHABET[(value % 36) as usize];
432        value /= 36;
433    }
434    String::from_utf8_lossy(&out).into_owned()
435}
436
437#[cfg(test)]
438mod tests {
439    use super::*;
440
441    #[test]
442    fn generated_ids_are_six_base36_chars() {
443        for _ in 0..100 {
444            let id = random_base36_6();
445            assert_eq!(id.len(), 6, "{id}");
446            assert!(
447                id.bytes()
448                    .all(|b| b.is_ascii_digit() || b.is_ascii_lowercase())
449            );
450        }
451    }
452
453    #[test]
454    fn generated_ids_avoid_collisions() {
455        let first = random_base36_6();
456        let reserved: HashSet<String> = [first.clone()].into_iter().collect();
457        assert_ne!(generate_id(&reserved, &HashMap::new()), first);
458    }
459}