Skip to main content

nectar_mantaray/
node.rs

1//! Node and Fork types for the mantaray trie.
2
3use std::collections::BTreeMap;
4use std::future::Future;
5use std::pin::Pin;
6
7use crate::error::{MantarayError, Result};
8use crate::mode::NodeEntry;
9use crate::obfuscation::ObfuscationKey;
10use crate::{PATH_SEPARATOR, PREFIX_MAX_LEN};
11use bytes::Bytes;
12use nectar_primitives::chunk::{Chunk, ChunkAddress, ContentChunk};
13use nectar_primitives::store::{ChunkGet, ChunkPut, MaybeSend};
14
15/// Boxed recursion future: `Send` on native, unbounded on wasm32 so `!Send`
16/// browser stores stay usable. `MaybeSend` cannot appear in a `dyn` bound
17/// directly (it is not an auto trait), so the auto trait is cfg-gated here.
18#[cfg(not(target_arch = "wasm32"))]
19type RecurseFuture<'a> = Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>>;
20#[cfg(target_arch = "wasm32")]
21type RecurseFuture<'a> = Pin<Box<dyn Future<Output = Result<()>> + 'a>>;
22
23/// Inline-only byte buffer for fork prefixes (max 30 bytes).
24///
25/// Always stores data inline; no heap allocation, no branching.
26/// 31 bytes total (1 len + 30 data).
27#[derive(Clone, PartialEq, Eq)]
28pub struct Prefix {
29    len: u8,
30    data: [u8; PREFIX_MAX_LEN],
31}
32
33impl Default for Prefix {
34    #[inline]
35    fn default() -> Self {
36        Self::new()
37    }
38}
39
40impl Prefix {
41    /// Maximum prefix length in bytes (constrained by the fork pre-reference region).
42    pub const MAX_LEN: usize = PREFIX_MAX_LEN;
43
44    /// Create an empty prefix.
45    #[inline]
46    pub const fn new() -> Self {
47        Self {
48            len: 0,
49            data: [0u8; PREFIX_MAX_LEN],
50        }
51    }
52
53    /// Create a prefix from a byte slice. Panics if `src.len() > 30`.
54    #[inline]
55    pub fn from_slice(src: &[u8]) -> Self {
56        debug_assert!(src.len() <= PREFIX_MAX_LEN);
57        let mut data = [0u8; PREFIX_MAX_LEN];
58        data[..src.len()].copy_from_slice(src);
59        Self {
60            len: src.len() as u8,
61            data,
62        }
63    }
64
65    /// Returns the prefix length in bytes.
66    #[inline]
67    pub const fn len(&self) -> usize {
68        self.len as usize
69    }
70
71    /// Returns true if the prefix is empty.
72    #[inline]
73    pub const fn is_empty(&self) -> bool {
74        self.len == 0
75    }
76
77    /// Returns the full 30-byte backing array (zero-padded beyond `len`).
78    #[inline]
79    pub const fn padded_bytes(&self) -> &[u8; PREFIX_MAX_LEN] {
80        &self.data
81    }
82}
83
84impl std::ops::Deref for Prefix {
85    type Target = [u8];
86
87    #[inline]
88    fn deref(&self) -> &[u8] {
89        &self.data[..self.len as usize]
90    }
91}
92
93impl std::fmt::Debug for Prefix {
94    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95        write!(f, "Prefix({:?})", &**self)
96    }
97}
98
99bitflags::bitflags! {
100    /// Bitflags encoding the kind of a mantaray node.
101    #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
102    pub struct NodeType: u8 {
103        /// Node stores a value (has an entry).
104        const VALUE = 2;
105        /// Node has child forks.
106        const EDGE = 4;
107        /// Path contains a "/" separator.
108        const PATH_SEPARATOR = 8;
109        /// Node has metadata key-value pairs.
110        const METADATA = 16;
111    }
112}
113
114/// A node in the mantaray trie.
115#[derive(Debug, Clone, PartialEq, Eq)]
116pub struct Node<E: NodeEntry = ChunkAddress> {
117    /// Bitflags encoding the node kind (value, edge, path-separator, metadata).
118    pub(crate) node_type: NodeType,
119    /// XOR obfuscation key for binary serialisation.
120    pub(crate) obfuscation_key: ObfuscationKey,
121    /// Content-addressed reference for this node (None if not yet persisted).
122    pub(crate) reference: Option<ChunkAddress>,
123    /// The typed entry stored at this node (the chunk reference this path maps to).
124    pub(crate) entry: Option<E>,
125    /// Metadata key-value pairs attached to this node.
126    pub(crate) metadata: BTreeMap<String, String>,
127    /// Child forks keyed by the first byte of their prefix.
128    pub(crate) forks: BTreeMap<u8, Fork<E>>,
129    /// Whether this node's forks have been loaded from storage.
130    pub(crate) loaded: bool,
131}
132
133impl<E: NodeEntry> Default for Node<E> {
134    fn default() -> Self {
135        Self {
136            node_type: NodeType::empty(),
137            obfuscation_key: ObfuscationKey::ZERO,
138            reference: None,
139            entry: None,
140            metadata: BTreeMap::new(),
141            forks: BTreeMap::new(),
142            loaded: false,
143        }
144    }
145}
146
147/// A fork in the mantaray trie, consisting of a prefix and a child node.
148#[derive(Debug, Clone, PartialEq, Eq)]
149pub struct Fork<E: NodeEntry = ChunkAddress> {
150    /// Inline-only prefix (max 30 bytes). No heap allocation, no branching.
151    pub(crate) prefix: Prefix,
152    /// The child node.
153    pub(crate) node: Node<E>,
154}
155
156impl<E: NodeEntry> Default for Fork<E> {
157    fn default() -> Self {
158        Self {
159            prefix: Prefix::new(),
160            node: Node::default(),
161        }
162    }
163}
164
165impl<E: NodeEntry> Fork<E> {
166    /// The prefix bytes for this fork edge.
167    pub fn prefix(&self) -> &[u8] {
168        &self.prefix
169    }
170
171    /// The child node.
172    pub const fn node(&self) -> &Node<E> {
173        &self.node
174    }
175
176    /// Mutable access to the child node.
177    pub const fn node_mut(&mut self) -> &mut Node<E> {
178        &mut self.node
179    }
180}
181
182/// Return the length of the common prefix of two byte slices.
183fn common_prefix_len(a: &[u8], b: &[u8]) -> usize {
184    a.iter().zip(b.iter()).take_while(|(x, y)| x == y).count()
185}
186
187impl<E: NodeEntry> Node<E> {
188    /// Create a new node with a zeroed obfuscation key (unencrypted).
189    pub fn new_unencrypted() -> Self {
190        Self {
191            obfuscation_key: ObfuscationKey::ZERO,
192            ..Default::default()
193        }
194    }
195
196    /// Create a node that references persisted data.
197    pub fn from_reference(reference: ChunkAddress) -> Self {
198        Self {
199            reference: Some(reference),
200            ..Default::default()
201        }
202    }
203
204    /// The typed entry stored at this node.
205    pub const fn entry(&self) -> Option<&E> {
206        self.entry.as_ref()
207    }
208
209    /// Metadata key-value pairs attached to this node.
210    pub const fn metadata(&self) -> &BTreeMap<String, String> {
211        &self.metadata
212    }
213
214    /// Mutable access to metadata for in-place modification.
215    pub(crate) const fn metadata_mut(&mut self) -> &mut BTreeMap<String, String> {
216        &mut self.metadata
217    }
218
219    /// Content-addressed reference for this node.
220    pub const fn reference(&self) -> Option<&ChunkAddress> {
221        self.reference.as_ref()
222    }
223
224    /// Child forks keyed by the first byte of their prefix.
225    pub const fn forks(&self) -> &BTreeMap<u8, Fork<E>> {
226        &self.forks
227    }
228
229    /// XOR obfuscation key for binary serialisation.
230    pub const fn obfuscation_key(&self) -> &ObfuscationKey {
231        &self.obfuscation_key
232    }
233
234    /// Check if the node has a value (entry).
235    pub const fn is_value(&self) -> bool {
236        self.node_type.contains(NodeType::VALUE)
237    }
238
239    /// Set the value flag.
240    pub(crate) const fn make_value(&mut self) {
241        self.node_type = self.node_type.union(NodeType::VALUE);
242    }
243
244    /// Check if the node has child forks.
245    pub const fn is_edge(&self) -> bool {
246        self.node_type.contains(NodeType::EDGE)
247    }
248
249    /// Set the edge flag.
250    pub(crate) const fn make_edge(&mut self) {
251        self.node_type = self.node_type.union(NodeType::EDGE);
252    }
253
254    /// Check if the path contains a separator.
255    pub const fn is_with_path_separator(&self) -> bool {
256        self.node_type.contains(NodeType::PATH_SEPARATOR)
257    }
258
259    /// Check if the node has metadata.
260    pub const fn is_with_metadata(&self) -> bool {
261        self.node_type.contains(NodeType::METADATA)
262    }
263
264    /// Set the metadata flag.
265    pub(crate) const fn make_with_metadata(&mut self) {
266        self.node_type = self.node_type.union(NodeType::METADATA);
267    }
268
269    fn update_is_with_path_separator(&mut self, path: &[u8]) {
270        let sep = PATH_SEPARATOR.as_bytes()[0];
271        if path.iter().skip(1).any(|&b| b == sep) {
272            self.node_type = self.node_type.union(NodeType::PATH_SEPARATOR);
273        } else {
274            self.node_type = self.node_type.difference(NodeType::PATH_SEPARATOR);
275        }
276    }
277
278    /// Clear persisted reference, marking this node for re-serialization on next save.
279    pub(crate) const fn mark_dirty(&mut self) {
280        self.reference = None;
281    }
282
283    /// Load forks from storage if the node hasn't been loaded yet.
284    async fn ensure_loaded<S: ChunkGet<BS>, const BS: usize>(&mut self, store: &S) -> Result<()> {
285        if !self.loaded {
286            self.load(store).await?;
287        }
288        Ok(())
289    }
290
291    /// Load this node from storage by its reference.
292    pub(crate) async fn load<S: ChunkGet<BS>, const BS: usize>(&mut self, store: &S) -> Result<()> {
293        let address = match self.reference {
294            Some(addr) => addr,
295            None => {
296                self.loaded = true;
297                return Ok(());
298            }
299        };
300
301        let chunk = store
302            .get(&address)
303            .await
304            .map_err(|e| MantarayError::StoreGet {
305                source: std::sync::Arc::new(e),
306            })?;
307        let mut loaded = Self::try_from(chunk.data().as_ref())?;
308        loaded.reference = Some(address);
309        // Preserve fields that live in the parent's fork data, not in this node's chunk:
310        // node_type flags and metadata key-value pairs.
311        loaded.node_type |= self.node_type;
312        loaded.metadata = core::mem::take(&mut self.metadata);
313        *self = loaded;
314        Ok(())
315    }
316
317    /// Look up the node at the given path, loading from storage as needed.
318    pub(crate) async fn lookup_node<S: ChunkGet<BS>, const BS: usize>(
319        &mut self,
320        path: &[u8],
321        store: &S,
322    ) -> Result<&mut Self> {
323        // Iterative descent: reborrow `current` to the chosen child each step.
324        let mut current = self;
325        let mut rest = path;
326        loop {
327            current.ensure_loaded(store).await?;
328
329            if rest.is_empty() {
330                return Ok(current);
331            }
332
333            let first = rest[0];
334            let reference = current.reference;
335            let fork = current
336                .forks
337                .get_mut(&first)
338                .ok_or(MantarayError::NoForkFound { reference })?;
339
340            let c = common_prefix_len(&fork.prefix, rest);
341            if c != fork.prefix.len() {
342                return Err(MantarayError::NoForkFound { reference });
343            }
344
345            current = &mut fork.node;
346            rest = &rest[c..];
347        }
348    }
349
350    /// Look up the entry at the given path, loading from storage as needed.
351    #[cfg(test)]
352    pub(crate) async fn lookup<S: ChunkGet<BS>, const BS: usize>(
353        &mut self,
354        path: &[u8],
355        store: &S,
356    ) -> Result<Option<&E>> {
357        let node = self.lookup_node(path, store).await?;
358        if !node.is_value() && !path.is_empty() {
359            return Err(MantarayError::NoEntryFound {
360                reference: node.reference,
361            });
362        }
363        Ok(node.entry.as_ref())
364    }
365
366    /// Add an entry at the given path with optional metadata, loading from storage as needed.
367    ///
368    /// Returns a boxed future so the `&mut self` recursion can name its own type.
369    /// The `MaybeSend` bound keeps `!Send` wasm stores usable.
370    pub(crate) fn add<'a, S: ChunkGet<BS>, const BS: usize>(
371        &'a mut self,
372        path: &'a [u8],
373        entry: Option<E>,
374        metadata: BTreeMap<String, String>,
375        store: &'a S,
376    ) -> RecurseFuture<'a>
377    where
378        E: MaybeSend,
379    {
380        Box::pin(async move {
381            // empty path; set this node as a value
382            if path.is_empty() {
383                self.entry = entry;
384                self.make_value();
385
386                if !metadata.is_empty() {
387                    self.metadata = metadata;
388                    self.make_with_metadata();
389                }
390
391                self.mark_dirty();
392                return Ok(());
393            }
394
395            // load forks if needed
396            if !self.loaded {
397                self.load(store).await?;
398                self.mark_dirty();
399            }
400
401            if !self.forks.contains_key(&path[0]) {
402                // no existing fork for this byte; create a new one
403                let mut nn = Self {
404                    obfuscation_key: self.obfuscation_key,
405                    ..Default::default()
406                };
407
408                if path.len() > PREFIX_MAX_LEN {
409                    let (prefix, rest) = path.split_at(PREFIX_MAX_LEN);
410                    nn.add(rest, entry, metadata, store).await?;
411                    nn.update_is_with_path_separator(prefix);
412                    self.forks.insert(
413                        path[0],
414                        Fork {
415                            prefix: Prefix::from_slice(prefix),
416                            node: nn,
417                        },
418                    );
419                    self.make_edge();
420                    return Ok(());
421                }
422
423                nn.entry = entry;
424                if !metadata.is_empty() {
425                    nn.metadata = metadata;
426                    nn.make_with_metadata();
427                }
428                nn.make_value();
429                nn.update_is_with_path_separator(path);
430
431                self.forks.insert(
432                    path[0],
433                    Fork {
434                        prefix: Prefix::from_slice(path),
435                        node: nn,
436                    },
437                );
438                self.make_edge();
439                return Ok(());
440            }
441
442            // existing fork; need to split or extend
443            let fork = self.forks.get(&path[0]).expect("checked above");
444            let c = common_prefix_len(&fork.prefix, path);
445            let rest = Prefix::from_slice(&fork.prefix[c..]);
446            let common_prefix = Prefix::from_slice(&fork.prefix[..c]);
447
448            // Take ownership; avoids cloning the entire node subtree
449            let old_fork = self.forks.remove(&path[0]).expect("checked above");
450
451            let mut nn = if rest.is_empty() {
452                old_fork.node
453            } else {
454                // split: create intermediate node
455                let mut intermediate = Self {
456                    obfuscation_key: self.obfuscation_key,
457                    ..Default::default()
458                };
459
460                let mut old_fork_node = old_fork.node;
461                old_fork_node.update_is_with_path_separator(&rest);
462                intermediate.forks.insert(
463                    rest[0],
464                    Fork {
465                        prefix: rest,
466                        node: old_fork_node,
467                    },
468                );
469                intermediate.make_edge();
470
471                if c == path.len() {
472                    intermediate.make_value();
473                }
474                intermediate
475            };
476
477            nn.update_is_with_path_separator(path);
478            nn.add(&path[c..], entry, metadata, store).await?;
479
480            self.forks.insert(
481                path[0],
482                Fork {
483                    prefix: common_prefix,
484                    node: nn,
485                },
486            );
487            self.make_edge();
488
489            Ok(())
490        })
491    }
492
493    /// Remove the entry at the given path, loading from storage as needed.
494    ///
495    /// Returns a boxed future so the `&mut self` recursion can name its own type.
496    pub(crate) fn remove<'a, S: ChunkGet<BS>, const BS: usize>(
497        &'a mut self,
498        path: &'a [u8],
499        store: &'a S,
500    ) -> RecurseFuture<'a>
501    where
502        E: MaybeSend,
503    {
504        Box::pin(async move {
505            if path.is_empty() {
506                return Err(MantarayError::EmptyPath);
507            }
508
509            self.ensure_loaded(store).await?;
510
511            let first = path[0];
512
513            // Clone prefix to release the borrow on self.forks
514            let prefix = match self.forks.get(&first) {
515                Some(f) => f.prefix.clone(),
516                None => {
517                    return Err(MantarayError::PathPrefixNotFound {
518                        prefix: String::from_utf8_lossy(&[first]).to_string(),
519                    });
520                }
521            };
522
523            if !path.starts_with(&prefix) {
524                return Err(MantarayError::PathPrefixNotFound {
525                    prefix: String::from_utf8_lossy(path).to_string(),
526                });
527            }
528
529            let rest = &path[prefix.len()..];
530            let result = if rest.is_empty() {
531                self.forks.remove(&first);
532                Ok(())
533            } else {
534                let fork = self.forks.get_mut(&first).expect("checked above");
535                fork.node.remove(rest, store).await
536            };
537
538            // Always clear reference so the node gets re-saved.
539            self.mark_dirty();
540            result
541        })
542    }
543
544    /// Test whether a prefix exists in the trie, loading from storage as needed.
545    pub(crate) async fn has_prefix<S: ChunkGet<BS>, const BS: usize>(
546        &mut self,
547        path: &[u8],
548        store: &S,
549    ) -> Result<bool> {
550        // Iterative descent: reborrow `current` to the chosen child each step.
551        let mut current = self;
552        let mut rest = path;
553        loop {
554            if rest.is_empty() {
555                return Ok(true);
556            }
557
558            current.ensure_loaded(store).await?;
559
560            let fork = match current.forks.get_mut(&rest[0]) {
561                Some(f) => f,
562                None => return Ok(false),
563            };
564
565            let c = common_prefix_len(&fork.prefix, rest);
566
567            if c == fork.prefix.len() {
568                current = &mut fork.node;
569                rest = &rest[c..];
570                continue;
571            }
572
573            if fork.prefix.starts_with(rest) {
574                return Ok(true);
575            }
576
577            return Ok(false);
578        }
579    }
580
581    /// Save this node and all children to storage in post-order.
582    ///
583    /// Uses BMT content-addressing via `ContentChunk`. An explicit stack avoids
584    /// recursion: each frame visits its forks (pushing unsaved children) before
585    /// the node itself is encoded and put.
586    pub(crate) async fn save<S: ChunkPut<BS>, const BS: usize>(&mut self, store: &S) -> Result<()> {
587        if self.reference.is_some() {
588            return Ok(());
589        }
590
591        struct SaveFrame<E: NodeEntry> {
592            /// Node owned by an ancestor's fork map, valid for this call.
593            node: *mut Node<E>,
594            /// Fork keys still to descend into.
595            keys: Vec<u8>,
596            /// Index into `keys`.
597            key_idx: usize,
598        }
599
600        let mut stack: Vec<SaveFrame<E>> = vec![SaveFrame {
601            node: self as *mut Self,
602            keys: self.forks.keys().copied().collect(),
603            key_idx: 0,
604        }];
605
606        while let Some(frame) = stack.last_mut() {
607            // SAFETY: every frame's node points into the exclusively borrowed
608            // trie. Children are only pushed once, then their parent waits in
609            // the stack below them, so no two frames alias the same node.
610            let node = unsafe { &mut *frame.node };
611
612            if frame.key_idx < frame.keys.len() {
613                let key = frame.keys[frame.key_idx];
614                frame.key_idx += 1;
615                let child = node.forks.get_mut(&key).expect("key from this node");
616                if child.node.reference.is_none() {
617                    let child_ptr = &mut child.node as *mut Self;
618                    let child_keys = child.node.forks.keys().copied().collect();
619                    stack.push(SaveFrame {
620                        node: child_ptr,
621                        keys: child_keys,
622                        key_idx: 0,
623                    });
624                }
625                continue;
626            }
627
628            // All children saved; encode and put this node, then pop.
629            let data = Vec::<u8>::try_from(&*node)?;
630            let chunk = ContentChunk::<BS>::new(Bytes::from(data))?;
631            let address = *chunk.address();
632            store
633                .put(chunk.into())
634                .await
635                .map_err(|e| MantarayError::StorePut {
636                    source: std::sync::Arc::new(e),
637                })?;
638            node.reference = Some(address);
639            node.forks.clear();
640            node.loaded = false;
641            stack.pop();
642        }
643
644        Ok(())
645    }
646
647    /// Walk all nodes depth-first, calling `f` for each node with its path.
648    pub(crate) async fn walk<S: ChunkGet<BS>, const BS: usize, F>(
649        &mut self,
650        store: &S,
651        f: &mut F,
652    ) -> Result<()>
653    where
654        F: FnMut(&[u8], &Self) -> Result<()>,
655    {
656        let mut path_buf = Vec::new();
657        walk_inner(&mut path_buf, self, store, f).await
658    }
659
660    /// Walk the subtree at `root`, calling `f` for each node.
661    pub(crate) async fn walk_from<S: ChunkGet<BS>, const BS: usize, F>(
662        &mut self,
663        root: &[u8],
664        store: &S,
665        f: &mut F,
666    ) -> Result<()>
667    where
668        F: FnMut(&[u8], &Self) -> Result<()>,
669    {
670        let mut path_buf = root.to_vec();
671        if root.is_empty() {
672            return walk_inner(&mut path_buf, self, store, f).await;
673        }
674
675        let target = self.lookup_node(root, store).await?;
676        walk_inner(&mut path_buf, target, store, f).await
677    }
678}
679
680/// Pre-order DFS visitor over a loaded-on-demand trie via an explicit stack.
681///
682/// The visitor `f` only reads loaded nodes, so it stays a synchronous `FnMut`.
683async fn walk_inner<E: NodeEntry, S: ChunkGet<BS>, const BS: usize, F>(
684    path_buf: &mut Vec<u8>,
685    node: &mut Node<E>,
686    store: &S,
687    f: &mut F,
688) -> Result<()>
689where
690    F: FnMut(&[u8], &Node<E>) -> Result<()>,
691{
692    struct WalkFrame {
693        /// Node visited at this level (raw pointer into the exclusive borrow).
694        node: *mut (),
695        /// Length of `path_buf` before this frame's prefix was appended.
696        path_len_before: usize,
697        /// Sorted fork keys for this node.
698        keys: Vec<u8>,
699        /// Index into `keys`.
700        key_idx: usize,
701    }
702
703    node.ensure_loaded(store).await?;
704    f(path_buf, node)?;
705
706    let mut stack: Vec<WalkFrame> = vec![WalkFrame {
707        node: (node as *mut Node<E>).cast::<()>(),
708        path_len_before: path_buf.len(),
709        keys: node.forks.keys().copied().collect(),
710        key_idx: 0,
711    }];
712
713    while let Some(frame) = stack.last_mut() {
714        if frame.key_idx >= frame.keys.len() {
715            path_buf.truncate(frame.path_len_before);
716            stack.pop();
717            continue;
718        }
719
720        let key = frame.keys[frame.key_idx];
721        frame.key_idx += 1;
722
723        // SAFETY: frame.node points into the exclusively borrowed trie. Each
724        // node appears in exactly one frame and is only dereferenced while at
725        // the top of the stack, so no two live references alias.
726        let parent = unsafe { &mut *frame.node.cast::<Node<E>>() };
727        let reference = parent.reference;
728        let fork = parent
729            .forks
730            .get_mut(&key)
731            .ok_or(MantarayError::NoForkFound { reference })?;
732
733        let prev_len = path_buf.len();
734        path_buf.extend_from_slice(&fork.prefix);
735
736        let child = &mut fork.node;
737        child.ensure_loaded(store).await?;
738        f(path_buf, child)?;
739
740        let child_ptr = (child as *mut Node<E>).cast::<()>();
741        let child_keys = child.forks.keys().copied().collect();
742        stack.push(WalkFrame {
743            node: child_ptr,
744            path_len_before: prev_len,
745            keys: child_keys,
746            key_idx: 0,
747        });
748    }
749
750    Ok(())
751}
752
753#[cfg(test)]
754mod tests {
755    use super::*;
756    use nectar_primitives::bmt::DEFAULT_BODY_SIZE;
757    use nectar_primitives::store::{MemoryStore, NullLoader};
758
759    struct TestCase {
760        _name: &'static str,
761        items: Vec<&'static str>,
762    }
763
764    #[derive(Default, Clone)]
765    struct RemoveTestCaseItem {
766        path: String,
767        metadata: BTreeMap<String, String>,
768    }
769
770    #[derive(Clone)]
771    struct RemoveTestCase {
772        _name: &'static str,
773        items: Vec<RemoveTestCaseItem>,
774        remove: Vec<String>,
775    }
776
777    #[derive(Clone)]
778    struct HasPrefixTestCase {
779        _name: &'static str,
780        paths: Vec<String>,
781        test_paths: Vec<String>,
782        should_exist: Vec<bool>,
783    }
784
785    fn test_case_data() -> [TestCase; 6] {
786        [
787            TestCase {
788                _name: "a",
789                items: vec![
790                    "aaaaaa", "aaaaab", "abbbb", "abbba", "bbbbba", "bbbaaa", "bbbaab", "aa", "b",
791                ],
792            },
793            TestCase {
794                _name: "simple",
795                items: vec!["/", "index.html", "img/1.png", "img/2.png", "robots.txt"],
796            },
797            TestCase {
798                _name: "nested-value-node-is-recognized",
799                items: vec![
800                    "..............................@",
801                    "..............................",
802                ],
803            },
804            TestCase {
805                _name: "nested-prefix-is-not-collapsed",
806                items: vec![
807                    "index.html",
808                    "img/1.png",
809                    "img/2/test1.png",
810                    "img/2/test2.png",
811                    "robots.txt",
812                ],
813            },
814            TestCase {
815                _name: "conflicting-path",
816                items: vec!["app.js.map", "app.js"],
817            },
818            TestCase {
819                _name: "spa-website",
820                items: vec![
821                    "css/",
822                    "css/app.css",
823                    "favicon.ico",
824                    "img/",
825                    "img/logo.png",
826                    "index.html",
827                    "js/",
828                    "js/chunk-vendors.js.map",
829                    "js/chunk-vendors.js",
830                    "js/app.js.map",
831                    "js/app.js",
832                ],
833            },
834        ]
835    }
836
837    fn remove_test_case_data() -> Vec<RemoveTestCase> {
838        vec![
839            RemoveTestCase {
840                _name: "simple",
841                items: vec![
842                    RemoveTestCaseItem {
843                        path: "/".to_string(),
844                        metadata: serde_json::from_str(r#"{"index-document": "index.html"}"#)
845                            .unwrap(),
846                    },
847                    RemoveTestCaseItem {
848                        path: "index.html".to_string(),
849                        ..Default::default()
850                    },
851                    RemoveTestCaseItem {
852                        path: "img/1.png".to_string(),
853                        ..Default::default()
854                    },
855                    RemoveTestCaseItem {
856                        path: "img/2.png".to_string(),
857                        ..Default::default()
858                    },
859                    RemoveTestCaseItem {
860                        path: "robots.txt".to_string(),
861                        ..Default::default()
862                    },
863                ],
864                remove: vec!["img/2.png".to_string()],
865            },
866            RemoveTestCase {
867                _name: "nested-prefix-is-not-collapsed",
868                items: vec![
869                    RemoveTestCaseItem {
870                        path: "index.html".to_string(),
871                        ..Default::default()
872                    },
873                    RemoveTestCaseItem {
874                        path: "img/1.png".to_string(),
875                        ..Default::default()
876                    },
877                    RemoveTestCaseItem {
878                        path: "img/2/test1.png".to_string(),
879                        ..Default::default()
880                    },
881                    RemoveTestCaseItem {
882                        path: "img/2/test2.png".to_string(),
883                        ..Default::default()
884                    },
885                    RemoveTestCaseItem {
886                        path: "robots.txt".to_string(),
887                        ..Default::default()
888                    },
889                ],
890                remove: vec!["img/2/test1.png".to_string()],
891            },
892        ]
893    }
894
895    fn has_prefix_test_case_data() -> Vec<HasPrefixTestCase> {
896        vec![
897            HasPrefixTestCase {
898                _name: "simple",
899                paths: vec![
900                    "index.html".to_string(),
901                    "img/1.png".to_string(),
902                    "img/2.png".to_string(),
903                    "robots.txt".to_string(),
904                ],
905                test_paths: vec!["img/".to_string(), "images/".to_string()],
906                should_exist: vec![true, false],
907            },
908            HasPrefixTestCase {
909                _name: "nested-single",
910                paths: vec!["some-path/file.ext".to_string()],
911                test_paths: vec![
912                    "some-path".to_string(),
913                    "some-path/file".to_string(),
914                    "some-other-path/".to_string(),
915                ],
916                should_exist: vec![true, true, false],
917            },
918        ]
919    }
920
921    use futures::executor::block_on;
922
923    const NL: NullLoader = NullLoader;
924    const BS: usize = DEFAULT_BODY_SIZE;
925
926    /// Create a 32-byte ChunkAddress from a string, left-padded with zeroes.
927    fn make_entry(s: &str) -> ChunkAddress {
928        let bytes = s.as_bytes();
929        let mut buf = [0u8; 32];
930        let start = 32 - bytes.len();
931        buf[start..].copy_from_slice(bytes);
932        ChunkAddress::from(buf)
933    }
934
935    /// In-memory add: delegates to `add` with NullLoader.
936    fn node_add(n: &mut Node, path: &[u8], entry: ChunkAddress, meta: BTreeMap<String, String>) {
937        block_on(n.add::<NullLoader, BS>(path, Some(entry), meta, &NL)).unwrap();
938    }
939
940    /// In-memory lookup: delegates to `lookup` with NullLoader.
941    fn node_lookup<'n>(n: &'n mut Node, path: &[u8]) -> Result<Option<&'n ChunkAddress>> {
942        block_on(n.lookup::<NullLoader, BS>(path, &NL))
943    }
944
945    /// In-memory lookup_node: delegates to `lookup_node` with NullLoader.
946    fn node_lookup_node<'n>(n: &'n mut Node, path: &[u8]) -> Result<&'n mut Node> {
947        block_on(n.lookup_node::<NullLoader, BS>(path, &NL))
948    }
949
950    /// In-memory remove: delegates to `remove` with NullLoader.
951    fn node_remove(n: &mut Node, path: &[u8]) -> Result<()> {
952        block_on(n.remove::<NullLoader, BS>(path, &NL))
953    }
954
955    /// In-memory has_prefix: delegates to `has_prefix` with NullLoader.
956    fn node_has_prefix(n: &mut Node, path: &[u8]) -> Result<bool> {
957        block_on(n.has_prefix::<NullLoader, BS>(path, &NL))
958    }
959
960    /// In-memory walk: delegates to `walk` with NullLoader.
961    fn node_walk<F>(n: &mut Node, f: &mut F) -> Result<()>
962    where
963        F: FnMut(&[u8], &Node) -> Result<()>,
964    {
965        block_on(n.walk::<NullLoader, BS, _>(&NL, f))
966    }
967
968    /// In-memory walk_node: delegates to `walk_from` with NullLoader.
969    fn node_walk_node<F>(n: &mut Node, root: &[u8], f: &mut F) -> Result<()>
970    where
971        F: FnMut(&[u8], &Node) -> Result<()>,
972    {
973        block_on(n.walk_from::<NullLoader, BS, _>(root, &NL, f))
974    }
975
976    #[test]
977    fn nil_path() {
978        let mut n = Node::default();
979        assert!(node_lookup(&mut n, b"").is_ok());
980    }
981
982    #[test]
983    fn add_and_lookup() {
984        let mut n = Node::default();
985        let items = &test_case_data()[0].items;
986
987        for (i, c) in items.iter().enumerate() {
988            let e = make_entry(c);
989            node_add(&mut n, c.as_bytes(), e, BTreeMap::new());
990
991            for &d in items.iter().take(i) {
992                let r = node_lookup(&mut n, d.as_bytes()).unwrap();
993                assert_eq!(r, Some(&make_entry(d)));
994            }
995        }
996    }
997
998    fn run_add_and_lookup_node(items: &[&str]) {
999        let mut n = Node::default();
1000
1001        for (i, c) in items.iter().enumerate() {
1002            let e = make_entry(c);
1003            node_add(&mut n, c.as_bytes(), e, BTreeMap::new());
1004
1005            for &d in items.iter().take(i) {
1006                let node = node_lookup_node(&mut n, d.as_bytes()).unwrap();
1007                assert!(node.is_value());
1008                assert_eq!(node.entry(), Some(&make_entry(d)));
1009            }
1010        }
1011    }
1012
1013    #[test]
1014    fn add_and_lookup_node_a() {
1015        run_add_and_lookup_node(&test_case_data()[0].items);
1016    }
1017
1018    #[test]
1019    fn add_and_lookup_node_simple() {
1020        run_add_and_lookup_node(&test_case_data()[1].items);
1021    }
1022
1023    #[test]
1024    fn add_and_lookup_node_nested_value() {
1025        run_add_and_lookup_node(&test_case_data()[2].items);
1026    }
1027
1028    #[test]
1029    fn add_and_lookup_node_nested_prefix() {
1030        run_add_and_lookup_node(&test_case_data()[3].items);
1031    }
1032
1033    #[test]
1034    fn add_and_lookup_node_conflicting_path() {
1035        run_add_and_lookup_node(&test_case_data()[4].items);
1036    }
1037
1038    #[test]
1039    fn add_and_lookup_node_spa_website() {
1040        run_add_and_lookup_node(&test_case_data()[5].items);
1041    }
1042
1043    fn run_add_and_lookup_with_load_save(items: &[&str]) {
1044        let mut n = Node::default();
1045
1046        for c in items {
1047            let e = make_entry(c);
1048            node_add(&mut n, c.as_bytes(), e, BTreeMap::new());
1049        }
1050
1051        let store = MemoryStore::<{ DEFAULT_BODY_SIZE }>::new();
1052        block_on(n.save(&store)).unwrap();
1053
1054        let mut n2: Node = Node::from_reference(n.reference.unwrap());
1055
1056        for &d in items {
1057            let node = block_on(n2.lookup_node(d.as_bytes(), &store)).unwrap();
1058            assert!(node.is_value());
1059            assert_eq!(node.entry(), Some(&make_entry(d)));
1060        }
1061    }
1062
1063    #[test]
1064    fn add_and_lookup_with_load_save_a() {
1065        run_add_and_lookup_with_load_save(&test_case_data()[0].items);
1066    }
1067
1068    #[test]
1069    fn add_and_lookup_with_load_save_simple() {
1070        run_add_and_lookup_with_load_save(&test_case_data()[1].items);
1071    }
1072
1073    #[test]
1074    fn add_and_lookup_with_load_save_nested_value() {
1075        run_add_and_lookup_with_load_save(&test_case_data()[2].items);
1076    }
1077
1078    #[test]
1079    fn add_and_lookup_with_load_save_nested_prefix() {
1080        run_add_and_lookup_with_load_save(&test_case_data()[3].items);
1081    }
1082
1083    #[test]
1084    fn add_and_lookup_with_load_save_conflicting_path() {
1085        run_add_and_lookup_with_load_save(&test_case_data()[4].items);
1086    }
1087
1088    #[test]
1089    fn add_and_lookup_with_load_save_spa_website() {
1090        run_add_and_lookup_with_load_save(&test_case_data()[5].items);
1091    }
1092
1093    fn run_remove(tc: RemoveTestCase) {
1094        let mut n = Node::default();
1095
1096        for (i, c) in tc.items.iter().enumerate() {
1097            let e = make_entry(&c.path);
1098            node_add(&mut n, c.path.as_bytes(), e, c.metadata.clone());
1099
1100            for item in tc.items.iter().take(i) {
1101                let r = node_lookup(&mut n, item.path.as_bytes()).unwrap();
1102                assert_eq!(r, Some(&make_entry(&item.path)));
1103            }
1104        }
1105
1106        for c in &tc.remove {
1107            node_remove(&mut n, c.as_bytes()).unwrap();
1108            assert!(node_lookup(&mut n, c.as_bytes()).is_err());
1109        }
1110    }
1111
1112    #[test]
1113    fn remove_simple() {
1114        run_remove(remove_test_case_data()[0].clone());
1115    }
1116
1117    #[test]
1118    fn remove_nested_prefix() {
1119        run_remove(remove_test_case_data()[1].clone());
1120    }
1121
1122    fn run_has_prefix(tc: HasPrefixTestCase) {
1123        let mut n = Node::default();
1124
1125        for c in &tc.paths {
1126            let e = make_entry(c);
1127            node_add(&mut n, c.as_bytes(), e, BTreeMap::default());
1128        }
1129
1130        for (i, test_prefix) in tc.test_paths.iter().enumerate() {
1131            assert_eq!(
1132                node_has_prefix(&mut n, test_prefix.as_bytes()).unwrap(),
1133                tc.should_exist[i],
1134            );
1135        }
1136    }
1137
1138    #[test]
1139    fn has_prefix_simple() {
1140        run_has_prefix(has_prefix_test_case_data()[0].clone());
1141    }
1142
1143    #[test]
1144    fn has_prefix_nested_single() {
1145        run_has_prefix(has_prefix_test_case_data()[1].clone());
1146    }
1147
1148    // Tests save->reload->remove->save->reload->verify-removed cycle.
1149
1150    fn run_persist_remove(tc: RemoveTestCase) {
1151        let store = MemoryStore::<{ DEFAULT_BODY_SIZE }>::new();
1152
1153        // add entries and persist
1154        let mut n = Node::default();
1155        for c in &tc.items {
1156            let e = make_entry(&c.path);
1157            block_on(n.add(c.path.as_bytes(), Some(e), c.metadata.clone(), &store)).unwrap();
1158        }
1159        block_on(n.save(&store)).unwrap();
1160        let ref_ = n.reference.unwrap();
1161
1162        // reload and remove
1163        let mut nn: Node = Node::from_reference(ref_);
1164        for path in &tc.remove {
1165            block_on(nn.remove(path.as_bytes(), &store)).unwrap();
1166        }
1167        block_on(nn.save(&store)).unwrap();
1168        let ref2 = nn.reference.unwrap();
1169
1170        // reload and verify removed paths are gone
1171        let mut nnn: Node = Node::from_reference(ref2);
1172        for path in &tc.remove {
1173            let result = block_on(nnn.lookup_node(path.as_bytes(), &store));
1174            assert!(
1175                result.is_err(),
1176                "expected removed path '{path}' to be not found"
1177            );
1178        }
1179    }
1180
1181    #[test]
1182    fn persist_remove_simple() {
1183        run_persist_remove(remove_test_case_data()[0].clone());
1184    }
1185
1186    #[test]
1187    fn persist_remove_nested_prefix() {
1188        run_persist_remove(remove_test_case_data()[1].clone());
1189    }
1190
1191    fn make_entry_bytes(s: &[u8]) -> ChunkAddress {
1192        let mut buf = [0u8; 32];
1193        let start = 32 - s.len();
1194        buf[start..].copy_from_slice(s);
1195        ChunkAddress::from(buf)
1196    }
1197
1198    #[test]
1199    fn walk_visits_all_nodes() {
1200        let mut root = Node::default();
1201
1202        let paths = &["index.html", "img/1.png", "img/2.png", "robots.txt"];
1203        for &p in paths {
1204            let entry = make_entry_bytes(p.as_bytes());
1205            node_add(&mut root, p.as_bytes(), entry, BTreeMap::new());
1206        }
1207
1208        let mut visited: Vec<(Vec<u8>, bool)> = Vec::new();
1209        node_walk(&mut root, &mut |path, node| {
1210            visited.push((path.to_vec(), node.is_value()));
1211            Ok(())
1212        })
1213        .unwrap();
1214
1215        for &p in paths {
1216            assert!(
1217                visited
1218                    .iter()
1219                    .any(|(vp, is_val)| vp == p.as_bytes() && *is_val),
1220                "path {p} not visited as value"
1221            );
1222        }
1223    }
1224
1225    #[test]
1226    fn walk_node_exact_order() {
1227        let to_add: &[&[u8]] = &[
1228            b"index.html.backup",
1229            b"index.html",
1230            b"img/test/oho.png",
1231            b"img/test/old/test.png.backup",
1232            b"img/test/old/test.png",
1233            b"img/2.png",
1234            b"img/1.png",
1235            b"robots.txt",
1236        ];
1237
1238        let expected: &[&[u8]] = &[
1239            b"",
1240            b"i",
1241            b"img/",
1242            b"img/1.png",
1243            b"img/2.png",
1244            b"img/test/o",
1245            b"img/test/oho.png",
1246            b"img/test/old/test.png",
1247            b"img/test/old/test.png.backup",
1248            b"index.html",
1249            b"index.html.backup",
1250            b"robots.txt",
1251        ];
1252
1253        let mut n = Node::default();
1254        for &path in to_add {
1255            let entry = make_entry_bytes(path);
1256            node_add(&mut n, path, entry, BTreeMap::new());
1257        }
1258
1259        let mut walked: Vec<Vec<u8>> = Vec::new();
1260        node_walk_node(&mut n, b"", &mut |path, _node| {
1261            walked.push(path.to_vec());
1262            Ok(())
1263        })
1264        .unwrap();
1265
1266        assert_eq!(
1267            walked.len(),
1268            expected.len(),
1269            "expected {} nodes, got {}",
1270            expected.len(),
1271            walked.len()
1272        );
1273
1274        for (i, (got, &want)) in walked.iter().zip(expected.iter()).enumerate() {
1275            assert_eq!(
1276                got.as_slice(),
1277                want,
1278                "walk step {i}: expected {:?}, got {:?}",
1279                core::str::from_utf8(want).unwrap_or("<non-utf8>"),
1280                core::str::from_utf8(got).unwrap_or("<non-utf8>"),
1281            );
1282        }
1283    }
1284
1285    #[test]
1286    fn walk_node_from_subtree() {
1287        let to_add: &[&[u8]] = &[b"index.html", b"img/1.png", b"img/2.png", b"robots.txt"];
1288
1289        let mut n = Node::default();
1290        for &path in to_add {
1291            let entry = make_entry_bytes(path);
1292            node_add(&mut n, path, entry, BTreeMap::new());
1293        }
1294
1295        let mut walked: Vec<Vec<u8>> = Vec::new();
1296        node_walk_node(&mut n, b"img/", &mut |path, _node| {
1297            walked.push(path.to_vec());
1298            Ok(())
1299        })
1300        .unwrap();
1301
1302        assert!(walked.iter().any(|p| p == b"img/1.png"));
1303        assert!(walked.iter().any(|p| p == b"img/2.png"));
1304        assert!(!walked.iter().any(|p| p == b"index.html"));
1305        assert!(!walked.iter().any(|p| p == b"robots.txt"));
1306    }
1307
1308    #[test]
1309    fn walk_node_exact_order_with_load_save() {
1310        let to_add: &[&[u8]] = &[
1311            b"index.html.backup",
1312            b"index.html",
1313            b"img/test/oho.png",
1314            b"img/test/old/test.png.backup",
1315            b"img/test/old/test.png",
1316            b"img/2.png",
1317            b"img/1.png",
1318            b"robots.txt",
1319        ];
1320
1321        let expected: &[&[u8]] = &[
1322            b"",
1323            b"i",
1324            b"img/",
1325            b"img/1.png",
1326            b"img/2.png",
1327            b"img/test/o",
1328            b"img/test/oho.png",
1329            b"img/test/old/test.png",
1330            b"img/test/old/test.png.backup",
1331            b"index.html",
1332            b"index.html.backup",
1333            b"robots.txt",
1334        ];
1335
1336        let mut n = Node::default();
1337        for &path in to_add {
1338            let entry = make_entry_bytes(path);
1339            node_add(&mut n, path, entry, BTreeMap::new());
1340        }
1341
1342        let store = MemoryStore::<{ DEFAULT_BODY_SIZE }>::new();
1343        block_on(n.save(&store)).unwrap();
1344
1345        let mut n2: Node = Node::from_reference(n.reference.unwrap());
1346
1347        let mut walked: Vec<Vec<u8>> = Vec::new();
1348        block_on(n2.walk_from(b"", &store, &mut |path: &[u8], _node: &Node| {
1349            walked.push(path.to_vec());
1350            Ok(())
1351        }))
1352        .unwrap();
1353
1354        assert_eq!(
1355            walked.len(),
1356            expected.len(),
1357            "expected {} nodes, got {}",
1358            expected.len(),
1359            walked.len()
1360        );
1361
1362        for (i, (got, &want)) in walked.iter().zip(expected.iter()).enumerate() {
1363            assert_eq!(
1364                got.as_slice(),
1365                want,
1366                "walk step {i}: expected {:?}, got {:?}",
1367                core::str::from_utf8(want).unwrap_or("<non-utf8>"),
1368                core::str::from_utf8(got).unwrap_or("<non-utf8>"),
1369            );
1370        }
1371    }
1372}