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        for key in pool.keys() {
209            reserved.remove(key);
210        }
211        validate_subtree(
212            std::slice::from_ref(&options),
213            &reserved,
214            &mut HashSet::new(),
215        )?;
216
217        let mut options = options;
218        options.id = Some(entry.id().to_owned());
219        let group = std::mem::take(&mut options.group);
220        entry.set_options(options);
221
222        // Detach first so a cross-group move never leaves the entry in two
223        // sibling lists at once.
224        detach_child(&old_parent, &entry);
225        let children = sync_children(
226            &entry,
227            group,
228            &mut pool,
229            &reserved,
230            &mut TreeDiff::default(),
231        );
232        entry.set_children(children);
233        insert_child(&parent, entry.clone(), position);
234        Ok(entry)
235    }
236
237    /// Reload the whole tree from new options, reusing existing entries
238    /// wherever ids match — including entries that moved between groups —
239    /// and returning what changed.
240    ///
241    /// Entries in the new data without an id cannot be matched and are
242    /// always created fresh; persist generated ids by writing
243    /// [`EntryTree::serialize`] back to the file.
244    pub fn update(&self, entries: Vec<EntryOptions>) -> Result<TreeDiff> {
245        let _guard = crate::lock(&self.mutation);
246        // Existing ids are reusable here, so nothing is reserved; only
247        // duplicates within the incoming data are rejected.
248        validate_subtree(&entries, &HashSet::new(), &mut HashSet::new())?;
249
250        // Index every existing entry by id so matches work across groups.
251        let mut pool: HashMap<String, Entry> = self
252            .entries()
253            .into_iter()
254            .map(|entry| (entry.id().to_string(), entry))
255            .collect();
256        let reserved = HashSet::new();
257        let mut diff = TreeDiff::default();
258        let children = sync_children(&self.root, entries, &mut pool, &reserved, &mut diff);
259        self.root.set_children(children);
260        diff.removed = pool.into_values().collect();
261        Ok(diff)
262    }
263
264    /// All entry ids currently in the tree.
265    fn ids(&self) -> HashSet<String> {
266        self.entries().iter().map(|e| e.id().to_string()).collect()
267    }
268
269    /// Fail unless `candidate` is the root of, or lives inside, this tree.
270    fn assert_owned(&self, candidate: &Entry) -> Result<()> {
271        let mut current = candidate.clone();
272        loop {
273            if Entry::ptr_eq(&current, &self.root) {
274                return Ok(());
275            }
276            match current.parent() {
277                Some(parent) => current = parent,
278                None => return Err(IncludeError::NotInTree),
279            }
280        }
281    }
282}
283
284/// Build the new child list for `parent` from `options`, taking reusable
285/// entries out of `pool` (leftovers become `removed`) and reporting
286/// mutations through `diff`. Infallible: callers pre-validate ids.
287fn sync_children(
288    parent: &Entry,
289    options: Vec<EntryOptions>,
290    pool: &mut HashMap<String, Entry>,
291    reserved: &HashSet<String>,
292    diff: &mut TreeDiff,
293) -> Vec<Entry> {
294    let mut result = Vec::with_capacity(options.len());
295    for options in options {
296        let mut options = options;
297        let id = match options.id.take() {
298            Some(id) => id,
299            None => generate_id(reserved, pool),
300        };
301        options.id = Some(id.clone());
302        let group = std::mem::take(&mut options.group);
303        let entry = match pool.remove(&id) {
304            Some(existing) => {
305                if existing.options() != options {
306                    existing.set_options(options);
307                    diff.updated.push(existing.clone());
308                }
309                let moved = existing
310                    .parent()
311                    .is_none_or(|old| !Entry::ptr_eq(&old, parent));
312                if moved {
313                    diff.moved.push(existing.clone());
314                }
315                existing
316            }
317            None => {
318                let created = Entry::new(id, options);
319                diff.created.push(created.clone());
320                created
321            }
322        };
323        let children = sync_children(&entry, group, pool, reserved, diff);
324        entry.set_children(children);
325        result.push(entry);
326    }
327    result
328}
329
330/// All entries strictly below `entry`, depth-first.
331fn descendants(entry: &Entry) -> Vec<Entry> {
332    let mut out = Vec::new();
333    fn walk(entry: &Entry, out: &mut Vec<Entry>) {
334        for child in entry.children() {
335            out.push(child.clone());
336            walk(&child, out);
337        }
338    }
339    walk(entry, &mut out);
340    out
341}
342
343/// Insert `child` into `parent`'s child list at `position` (append when
344/// `None`, clamped to the ends).
345fn insert_child(parent: &Entry, child: Entry, position: Option<usize>) {
346    let mut siblings = parent.children();
347    let index = position.unwrap_or(siblings.len()).min(siblings.len());
348    siblings.insert(index, child);
349    parent.set_children(siblings);
350}
351
352/// Remove `child` from `parent`'s child list, leaving the child's own
353/// subtree intact and clearing its parent link.
354fn detach_child(parent: &Entry, child: &Entry) {
355    let kept: Vec<Entry> = parent
356        .children()
357        .into_iter()
358        .filter(|kept| !Entry::ptr_eq(kept, child))
359        .collect();
360    parent.set_children(kept);
361}
362
363/// Reject empty ids and the `:` path separator.
364fn validate_id(id: &str) -> Result<()> {
365    if id.is_empty() || id.contains(':') {
366        return Err(IncludeError::InvalidId { id: id.to_owned() });
367    }
368    Ok(())
369}
370
371/// Pre-validate an incoming options tree before any mutation: non-empty
372/// names, well-formed ids, no duplicate explicit ids within the data, and
373/// no collision with `reserved` (ids outside the reusable pool).
374fn validate_subtree(
375    entries: &[EntryOptions],
376    reserved: &HashSet<String>,
377    seen: &mut HashSet<String>,
378) -> Result<()> {
379    for options in entries {
380        if options.name.is_empty() {
381            return Err(IncludeError::InvalidName);
382        }
383        if let Some(id) = options.id.as_deref() {
384            validate_id(id)?;
385            if reserved.contains(id) {
386                return Err(IncludeError::DuplicateId { id: id.to_owned() });
387            }
388            if !seen.insert(id.to_owned()) {
389                return Err(IncludeError::DuplicateId { id: id.to_owned() });
390            }
391        }
392        validate_subtree(&options.group, reserved, seen)?;
393    }
394    Ok(())
395}
396
397/// Generate a random 6-character base36 id avoiding `reserved` ids and
398/// anything still pooled.
399fn generate_id(reserved: &HashSet<String>, pool: &HashMap<String, Entry>) -> String {
400    loop {
401        let candidate = random_base36_6();
402        if !reserved.contains(&candidate) && !pool.contains_key(&candidate) {
403            return candidate;
404        }
405    }
406}
407
408/// Six base36 characters from a time/counter-seeded splitmix64 stream.
409/// Uniqueness matters, unpredictability does not.
410fn random_base36_6() -> String {
411    use std::sync::atomic::{AtomicU64, Ordering};
412    static COUNTER: AtomicU64 = AtomicU64::new(0);
413
414    let nanos = SystemTime::now()
415        .duration_since(UNIX_EPOCH)
416        .map(|elapsed| elapsed.as_nanos() as u64)
417        .unwrap_or(0);
418    let count = COUNTER.fetch_add(1, Ordering::Relaxed);
419    let mut z = nanos ^ count.wrapping_mul(0x9E37_79B9_7F4A_7C15);
420    z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
421    z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
422    z ^= z >> 31;
423
424    const ALPHABET: &[u8; 36] = b"0123456789abcdefghijklmnopqrstuvwxyz";
425    let mut value = z % 2_176_782_336; // 36^6
426    let mut out = [0u8; 6];
427    for slot in out.iter_mut().rev() {
428        *slot = ALPHABET[(value % 36) as usize];
429        value /= 36;
430    }
431    String::from_utf8_lossy(&out).into_owned()
432}
433
434#[cfg(test)]
435mod tests {
436    use super::*;
437
438    #[test]
439    fn generated_ids_are_six_base36_chars() {
440        for _ in 0..100 {
441            let id = random_base36_6();
442            assert_eq!(id.len(), 6, "{id}");
443            assert!(
444                id.bytes()
445                    .all(|b| b.is_ascii_digit() || b.is_ascii_lowercase())
446            );
447        }
448    }
449
450    #[test]
451    fn generated_ids_avoid_collisions() {
452        let first = random_base36_6();
453        let reserved: HashSet<String> = [first.clone()].into_iter().collect();
454        assert_ne!(generate_id(&reserved, &HashMap::new()), first);
455    }
456}