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