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