1use std::collections::BTreeMap;
4
5use crate::error::{MantarayError, Result};
6use crate::mode::NodeEntry;
7use crate::obfuscation::ObfuscationKey;
8use crate::{PATH_SEPARATOR, PREFIX_MAX_LEN};
9use bytes::Bytes;
10use nectar_primitives::chunk::{Chunk, ChunkAddress, ContentChunk};
11use nectar_primitives::store::{SyncChunkGet, SyncChunkPut};
12
13#[derive(Clone, PartialEq, Eq)]
18pub struct Prefix {
19 len: u8,
20 data: [u8; PREFIX_MAX_LEN],
21}
22
23impl Default for Prefix {
24 #[inline]
25 fn default() -> Self {
26 Self::new()
27 }
28}
29
30impl Prefix {
31 pub const MAX_LEN: usize = PREFIX_MAX_LEN;
33
34 #[inline]
36 pub const fn new() -> Self {
37 Self {
38 len: 0,
39 data: [0u8; PREFIX_MAX_LEN],
40 }
41 }
42
43 #[inline]
45 pub fn from_slice(src: &[u8]) -> Self {
46 debug_assert!(src.len() <= PREFIX_MAX_LEN);
47 let mut data = [0u8; PREFIX_MAX_LEN];
48 data[..src.len()].copy_from_slice(src);
49 Self {
50 len: src.len() as u8,
51 data,
52 }
53 }
54
55 #[inline]
57 pub const fn len(&self) -> usize {
58 self.len as usize
59 }
60
61 #[inline]
63 pub const fn is_empty(&self) -> bool {
64 self.len == 0
65 }
66
67 #[inline]
69 pub const fn padded_bytes(&self) -> &[u8; PREFIX_MAX_LEN] {
70 &self.data
71 }
72}
73
74impl std::ops::Deref for Prefix {
75 type Target = [u8];
76
77 #[inline]
78 fn deref(&self) -> &[u8] {
79 &self.data[..self.len as usize]
80 }
81}
82
83impl std::fmt::Debug for Prefix {
84 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85 write!(f, "Prefix({:?})", &**self)
86 }
87}
88
89bitflags::bitflags! {
90 #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
92 pub struct NodeType: u8 {
93 const VALUE = 2;
95 const EDGE = 4;
97 const PATH_SEPARATOR = 8;
99 const METADATA = 16;
101 }
102}
103
104#[derive(Debug, Clone, PartialEq, Eq)]
106pub struct Node<E: NodeEntry = ChunkAddress> {
107 pub(crate) node_type: NodeType,
109 pub(crate) obfuscation_key: ObfuscationKey,
111 pub(crate) reference: Option<ChunkAddress>,
113 pub(crate) entry: Option<E>,
115 pub(crate) metadata: BTreeMap<String, String>,
117 pub(crate) forks: BTreeMap<u8, Fork<E>>,
119 pub(crate) loaded: bool,
121}
122
123impl<E: NodeEntry> Default for Node<E> {
124 fn default() -> Self {
125 Self {
126 node_type: NodeType::empty(),
127 obfuscation_key: ObfuscationKey::ZERO,
128 reference: None,
129 entry: None,
130 metadata: BTreeMap::new(),
131 forks: BTreeMap::new(),
132 loaded: false,
133 }
134 }
135}
136
137#[derive(Debug, Clone, PartialEq, Eq)]
139pub struct Fork<E: NodeEntry = ChunkAddress> {
140 pub(crate) prefix: Prefix,
142 pub(crate) node: Node<E>,
144}
145
146impl<E: NodeEntry> Default for Fork<E> {
147 fn default() -> Self {
148 Self {
149 prefix: Prefix::new(),
150 node: Node::default(),
151 }
152 }
153}
154
155impl<E: NodeEntry> Fork<E> {
156 pub fn prefix(&self) -> &[u8] {
158 &self.prefix
159 }
160
161 pub const fn node(&self) -> &Node<E> {
163 &self.node
164 }
165
166 pub const fn node_mut(&mut self) -> &mut Node<E> {
168 &mut self.node
169 }
170}
171
172fn common_prefix_len(a: &[u8], b: &[u8]) -> usize {
174 a.iter().zip(b.iter()).take_while(|(x, y)| x == y).count()
175}
176
177impl<E: NodeEntry> Node<E> {
178 pub fn new_unencrypted() -> Self {
180 Self {
181 obfuscation_key: ObfuscationKey::ZERO,
182 ..Default::default()
183 }
184 }
185
186 pub fn from_reference(reference: ChunkAddress) -> Self {
188 Self {
189 reference: Some(reference),
190 ..Default::default()
191 }
192 }
193
194 pub const fn entry(&self) -> Option<&E> {
196 self.entry.as_ref()
197 }
198
199 pub const fn metadata(&self) -> &BTreeMap<String, String> {
201 &self.metadata
202 }
203
204 pub(crate) const fn metadata_mut(&mut self) -> &mut BTreeMap<String, String> {
206 &mut self.metadata
207 }
208
209 pub const fn reference(&self) -> Option<&ChunkAddress> {
211 self.reference.as_ref()
212 }
213
214 pub const fn forks(&self) -> &BTreeMap<u8, Fork<E>> {
216 &self.forks
217 }
218
219 pub const fn obfuscation_key(&self) -> &ObfuscationKey {
221 &self.obfuscation_key
222 }
223
224 pub const fn is_value(&self) -> bool {
226 self.node_type.contains(NodeType::VALUE)
227 }
228
229 pub(crate) const fn make_value(&mut self) {
231 self.node_type = self.node_type.union(NodeType::VALUE);
232 }
233
234 pub const fn is_edge(&self) -> bool {
236 self.node_type.contains(NodeType::EDGE)
237 }
238
239 pub(crate) const fn make_edge(&mut self) {
241 self.node_type = self.node_type.union(NodeType::EDGE);
242 }
243
244 pub const fn is_with_path_separator(&self) -> bool {
246 self.node_type.contains(NodeType::PATH_SEPARATOR)
247 }
248
249 pub const fn is_with_metadata(&self) -> bool {
251 self.node_type.contains(NodeType::METADATA)
252 }
253
254 pub(crate) const fn make_with_metadata(&mut self) {
256 self.node_type = self.node_type.union(NodeType::METADATA);
257 }
258
259 fn update_is_with_path_separator(&mut self, path: &[u8]) {
260 let sep = PATH_SEPARATOR.as_bytes()[0];
261 if path.iter().skip(1).any(|&b| b == sep) {
262 self.node_type = self.node_type.union(NodeType::PATH_SEPARATOR);
263 } else {
264 self.node_type = self.node_type.difference(NodeType::PATH_SEPARATOR);
265 }
266 }
267
268 pub(crate) const fn mark_dirty(&mut self) {
270 self.reference = None;
271 }
272
273 fn ensure_loaded<S: SyncChunkGet<BS>, const BS: usize>(&mut self, store: &S) -> Result<()> {
275 if !self.loaded {
276 self.load(store)?;
277 }
278 Ok(())
279 }
280
281 pub(crate) fn load<S: SyncChunkGet<BS>, const BS: usize>(&mut self, store: &S) -> Result<()> {
283 let address = match self.reference {
284 Some(addr) => addr,
285 None => {
286 self.loaded = true;
287 return Ok(());
288 }
289 };
290
291 let chunk = store.get(&address).map_err(|e| MantarayError::StoreGet {
292 source: std::sync::Arc::new(e),
293 })?;
294 let mut loaded = Self::try_from(chunk.data().as_ref())?;
295 loaded.reference = Some(address);
296 loaded.node_type |= self.node_type;
299 loaded.metadata = core::mem::take(&mut self.metadata);
300 *self = loaded;
301 Ok(())
302 }
303
304 pub(crate) fn lookup_node<S: SyncChunkGet<BS>, const BS: usize>(
306 &mut self,
307 path: &[u8],
308 store: &S,
309 ) -> Result<&mut Self> {
310 self.ensure_loaded(store)?;
311
312 if path.is_empty() {
313 return Ok(self);
314 }
315
316 let first = path[0];
317 let fork = self
318 .forks
319 .get_mut(&first)
320 .ok_or_else(|| MantarayError::NoForkFound {
321 reference: self.reference,
322 })?;
323
324 let c = common_prefix_len(&fork.prefix, path);
325 if c == fork.prefix.len() {
326 fork.node.lookup_node(&path[c..], store)
327 } else {
328 Err(MantarayError::NoForkFound {
329 reference: self.reference,
330 })
331 }
332 }
333
334 #[cfg(test)]
336 pub(crate) fn lookup<S: SyncChunkGet<BS>, const BS: usize>(
337 &mut self,
338 path: &[u8],
339 store: &S,
340 ) -> Result<Option<&E>> {
341 let node = self.lookup_node(path, store)?;
342 if !node.is_value() && !path.is_empty() {
343 return Err(MantarayError::NoEntryFound {
344 reference: node.reference,
345 });
346 }
347 Ok(node.entry.as_ref())
348 }
349
350 pub(crate) fn add<S: SyncChunkGet<BS>, const BS: usize>(
352 &mut self,
353 path: &[u8],
354 entry: Option<E>,
355 metadata: BTreeMap<String, String>,
356 store: &S,
357 ) -> Result<()> {
358 if path.is_empty() {
360 self.entry = entry;
361 self.make_value();
362
363 if !metadata.is_empty() {
364 self.metadata = metadata;
365 self.make_with_metadata();
366 }
367
368 self.mark_dirty();
369 return Ok(());
370 }
371
372 if !self.loaded {
374 self.load(store)?;
375 self.mark_dirty();
376 }
377
378 if !self.forks.contains_key(&path[0]) {
379 let mut nn = Self {
381 obfuscation_key: self.obfuscation_key,
382 ..Default::default()
383 };
384
385 if path.len() > PREFIX_MAX_LEN {
386 let (prefix, rest) = path.split_at(PREFIX_MAX_LEN);
387 nn.add(rest, entry, metadata, store)?;
388 nn.update_is_with_path_separator(prefix);
389 self.forks.insert(
390 path[0],
391 Fork {
392 prefix: Prefix::from_slice(prefix),
393 node: nn,
394 },
395 );
396 self.make_edge();
397 return Ok(());
398 }
399
400 nn.entry = entry;
401 if !metadata.is_empty() {
402 nn.metadata = metadata;
403 nn.make_with_metadata();
404 }
405 nn.make_value();
406 nn.update_is_with_path_separator(path);
407
408 self.forks.insert(
409 path[0],
410 Fork {
411 prefix: Prefix::from_slice(path),
412 node: nn,
413 },
414 );
415 self.make_edge();
416 return Ok(());
417 }
418
419 let fork = self.forks.get(&path[0]).expect("checked above");
421 let c = common_prefix_len(&fork.prefix, path);
422 let rest = Prefix::from_slice(&fork.prefix[c..]);
423 let common_prefix = Prefix::from_slice(&fork.prefix[..c]);
424
425 let old_fork = self.forks.remove(&path[0]).expect("checked above");
427
428 let mut nn = if rest.is_empty() {
429 old_fork.node
430 } else {
431 let mut intermediate = Self {
433 obfuscation_key: self.obfuscation_key,
434 ..Default::default()
435 };
436
437 let mut old_fork_node = old_fork.node;
438 old_fork_node.update_is_with_path_separator(&rest);
439 intermediate.forks.insert(
440 rest[0],
441 Fork {
442 prefix: rest,
443 node: old_fork_node,
444 },
445 );
446 intermediate.make_edge();
447
448 if c == path.len() {
449 intermediate.make_value();
450 }
451 intermediate
452 };
453
454 nn.update_is_with_path_separator(path);
455 nn.add(&path[c..], entry, metadata, store)?;
456
457 self.forks.insert(
458 path[0],
459 Fork {
460 prefix: common_prefix,
461 node: nn,
462 },
463 );
464 self.make_edge();
465
466 Ok(())
467 }
468
469 pub(crate) fn remove<S: SyncChunkGet<BS>, const BS: usize>(
471 &mut self,
472 path: &[u8],
473 store: &S,
474 ) -> Result<()> {
475 if path.is_empty() {
476 return Err(MantarayError::EmptyPath);
477 }
478
479 self.ensure_loaded(store)?;
480
481 let first = path[0];
482
483 let prefix = match self.forks.get(&first) {
485 Some(f) => f.prefix.clone(),
486 None => {
487 return Err(MantarayError::PathPrefixNotFound {
488 prefix: String::from_utf8_lossy(&[first]).to_string(),
489 });
490 }
491 };
492
493 if !path.starts_with(&prefix) {
494 return Err(MantarayError::PathPrefixNotFound {
495 prefix: String::from_utf8_lossy(path).to_string(),
496 });
497 }
498
499 let rest = &path[prefix.len()..];
500 let result = if rest.is_empty() {
501 self.forks.remove(&first);
502 Ok(())
503 } else {
504 let fork = self.forks.get_mut(&first).expect("checked above");
505 fork.node.remove(rest, store)
506 };
507
508 self.mark_dirty();
510 result
511 }
512
513 pub(crate) fn has_prefix<S: SyncChunkGet<BS>, const BS: usize>(
515 &mut self,
516 path: &[u8],
517 store: &S,
518 ) -> Result<bool> {
519 if path.is_empty() {
520 return Ok(true);
521 }
522
523 self.ensure_loaded(store)?;
524
525 let fork = match self.forks.get_mut(&path[0]) {
526 Some(f) => f,
527 None => return Ok(false),
528 };
529
530 let c = common_prefix_len(&fork.prefix, path);
531
532 if c == fork.prefix.len() {
533 return fork.node.has_prefix(&path[c..], store);
534 }
535
536 if fork.prefix.starts_with(path) {
537 return Ok(true);
538 }
539
540 Ok(false)
541 }
542
543 pub(crate) fn save<S: SyncChunkPut<BS>, const BS: usize>(&mut self, store: &S) -> Result<()> {
547 if self.reference.is_some() {
548 return Ok(());
549 }
550
551 for fork in self.forks.values_mut() {
552 fork.node.save(store)?;
553 }
554
555 let data = Vec::<u8>::try_from(&*self)?;
556 let chunk = ContentChunk::<BS>::new(Bytes::from(data))?;
557 let address = *chunk.address();
558 store
559 .put(chunk.into())
560 .map_err(|e| MantarayError::StorePut {
561 source: std::sync::Arc::new(e),
562 })?;
563 self.reference = Some(address);
564 self.forks.clear();
565 self.loaded = false;
566
567 Ok(())
568 }
569
570 pub(crate) fn walk<S: SyncChunkGet<BS>, const BS: usize, F>(
572 &mut self,
573 store: &S,
574 f: &mut F,
575 ) -> Result<()>
576 where
577 F: FnMut(&[u8], &Self) -> Result<()>,
578 {
579 let mut path_buf = Vec::new();
580 walk_inner(&mut path_buf, self, store, f)
581 }
582
583 pub(crate) fn walk_from<S: SyncChunkGet<BS>, const BS: usize, F>(
585 &mut self,
586 root: &[u8],
587 store: &S,
588 f: &mut F,
589 ) -> Result<()>
590 where
591 F: FnMut(&[u8], &Self) -> Result<()>,
592 {
593 let mut path_buf = root.to_vec();
594 if root.is_empty() {
595 return walk_inner(&mut path_buf, self, store, f);
596 }
597
598 let target = self.lookup_node(root, store)?;
599 walk_inner(&mut path_buf, target, store, f)
600 }
601}
602
603fn walk_inner<E: NodeEntry, S: SyncChunkGet<BS>, const BS: usize, F>(
604 path_buf: &mut Vec<u8>,
605 node: &mut Node<E>,
606 store: &S,
607 f: &mut F,
608) -> Result<()>
609where
610 F: FnMut(&[u8], &Node<E>) -> Result<()>,
611{
612 node.ensure_loaded(store)?;
613
614 f(path_buf, node)?;
615
616 let keys: Vec<u8> = node.forks.keys().copied().collect();
618 for key in keys {
619 let fork = node
620 .forks
621 .get_mut(&key)
622 .ok_or_else(|| MantarayError::NoForkFound {
623 reference: node.reference,
624 })?;
625 let prev_len = path_buf.len();
626 path_buf.extend_from_slice(&fork.prefix);
627 walk_inner(path_buf, &mut fork.node, store, f)?;
628 path_buf.truncate(prev_len);
629 }
630
631 Ok(())
632}
633
634#[cfg(test)]
635mod tests {
636 use super::*;
637 use nectar_primitives::bmt::DEFAULT_BODY_SIZE;
638 use nectar_primitives::store::{MemoryStore, NullLoader};
639
640 struct TestCase {
641 _name: &'static str,
642 items: Vec<&'static str>,
643 }
644
645 #[derive(Default, Clone)]
646 struct RemoveTestCaseItem {
647 path: String,
648 metadata: BTreeMap<String, String>,
649 }
650
651 #[derive(Clone)]
652 struct RemoveTestCase {
653 _name: &'static str,
654 items: Vec<RemoveTestCaseItem>,
655 remove: Vec<String>,
656 }
657
658 #[derive(Clone)]
659 struct HasPrefixTestCase {
660 _name: &'static str,
661 paths: Vec<String>,
662 test_paths: Vec<String>,
663 should_exist: Vec<bool>,
664 }
665
666 fn test_case_data() -> [TestCase; 6] {
667 [
668 TestCase {
669 _name: "a",
670 items: vec![
671 "aaaaaa", "aaaaab", "abbbb", "abbba", "bbbbba", "bbbaaa", "bbbaab", "aa", "b",
672 ],
673 },
674 TestCase {
675 _name: "simple",
676 items: vec!["/", "index.html", "img/1.png", "img/2.png", "robots.txt"],
677 },
678 TestCase {
679 _name: "nested-value-node-is-recognized",
680 items: vec![
681 "..............................@",
682 "..............................",
683 ],
684 },
685 TestCase {
686 _name: "nested-prefix-is-not-collapsed",
687 items: vec![
688 "index.html",
689 "img/1.png",
690 "img/2/test1.png",
691 "img/2/test2.png",
692 "robots.txt",
693 ],
694 },
695 TestCase {
696 _name: "conflicting-path",
697 items: vec!["app.js.map", "app.js"],
698 },
699 TestCase {
700 _name: "spa-website",
701 items: vec![
702 "css/",
703 "css/app.css",
704 "favicon.ico",
705 "img/",
706 "img/logo.png",
707 "index.html",
708 "js/",
709 "js/chunk-vendors.js.map",
710 "js/chunk-vendors.js",
711 "js/app.js.map",
712 "js/app.js",
713 ],
714 },
715 ]
716 }
717
718 fn remove_test_case_data() -> Vec<RemoveTestCase> {
719 vec![
720 RemoveTestCase {
721 _name: "simple",
722 items: vec![
723 RemoveTestCaseItem {
724 path: "/".to_string(),
725 metadata: serde_json::from_str(r#"{"index-document": "index.html"}"#)
726 .unwrap(),
727 },
728 RemoveTestCaseItem {
729 path: "index.html".to_string(),
730 ..Default::default()
731 },
732 RemoveTestCaseItem {
733 path: "img/1.png".to_string(),
734 ..Default::default()
735 },
736 RemoveTestCaseItem {
737 path: "img/2.png".to_string(),
738 ..Default::default()
739 },
740 RemoveTestCaseItem {
741 path: "robots.txt".to_string(),
742 ..Default::default()
743 },
744 ],
745 remove: vec!["img/2.png".to_string()],
746 },
747 RemoveTestCase {
748 _name: "nested-prefix-is-not-collapsed",
749 items: vec![
750 RemoveTestCaseItem {
751 path: "index.html".to_string(),
752 ..Default::default()
753 },
754 RemoveTestCaseItem {
755 path: "img/1.png".to_string(),
756 ..Default::default()
757 },
758 RemoveTestCaseItem {
759 path: "img/2/test1.png".to_string(),
760 ..Default::default()
761 },
762 RemoveTestCaseItem {
763 path: "img/2/test2.png".to_string(),
764 ..Default::default()
765 },
766 RemoveTestCaseItem {
767 path: "robots.txt".to_string(),
768 ..Default::default()
769 },
770 ],
771 remove: vec!["img/2/test1.png".to_string()],
772 },
773 ]
774 }
775
776 fn has_prefix_test_case_data() -> Vec<HasPrefixTestCase> {
777 vec![
778 HasPrefixTestCase {
779 _name: "simple",
780 paths: vec![
781 "index.html".to_string(),
782 "img/1.png".to_string(),
783 "img/2.png".to_string(),
784 "robots.txt".to_string(),
785 ],
786 test_paths: vec!["img/".to_string(), "images/".to_string()],
787 should_exist: vec![true, false],
788 },
789 HasPrefixTestCase {
790 _name: "nested-single",
791 paths: vec!["some-path/file.ext".to_string()],
792 test_paths: vec![
793 "some-path".to_string(),
794 "some-path/file".to_string(),
795 "some-other-path/".to_string(),
796 ],
797 should_exist: vec![true, true, false],
798 },
799 ]
800 }
801
802 const NL: NullLoader = NullLoader;
803 const BS: usize = DEFAULT_BODY_SIZE;
804
805 fn make_entry(s: &str) -> ChunkAddress {
807 let bytes = s.as_bytes();
808 let mut buf = [0u8; 32];
809 let start = 32 - bytes.len();
810 buf[start..].copy_from_slice(bytes);
811 ChunkAddress::from(buf)
812 }
813
814 fn node_add(n: &mut Node, path: &[u8], entry: ChunkAddress, meta: BTreeMap<String, String>) {
816 n.add::<NullLoader, BS>(path, Some(entry), meta, &NL)
817 .unwrap();
818 }
819
820 fn node_lookup<'n>(n: &'n mut Node, path: &[u8]) -> Result<Option<&'n ChunkAddress>> {
822 n.lookup::<NullLoader, BS>(path, &NL)
823 }
824
825 fn node_lookup_node<'n>(n: &'n mut Node, path: &[u8]) -> Result<&'n mut Node> {
827 n.lookup_node::<NullLoader, BS>(path, &NL)
828 }
829
830 fn node_remove(n: &mut Node, path: &[u8]) -> Result<()> {
832 n.remove::<NullLoader, BS>(path, &NL)
833 }
834
835 fn node_has_prefix(n: &mut Node, path: &[u8]) -> Result<bool> {
837 n.has_prefix::<NullLoader, BS>(path, &NL)
838 }
839
840 fn node_walk<F>(n: &mut Node, f: &mut F) -> Result<()>
842 where
843 F: FnMut(&[u8], &Node) -> Result<()>,
844 {
845 n.walk::<NullLoader, BS, _>(&NL, f)
846 }
847
848 fn node_walk_node<F>(n: &mut Node, root: &[u8], f: &mut F) -> Result<()>
850 where
851 F: FnMut(&[u8], &Node) -> Result<()>,
852 {
853 n.walk_from::<NullLoader, BS, _>(root, &NL, f)
854 }
855
856 #[test]
857 fn nil_path() {
858 let mut n = Node::default();
859 assert!(node_lookup(&mut n, b"").is_ok());
860 }
861
862 #[test]
863 fn add_and_lookup() {
864 let mut n = Node::default();
865 let items = &test_case_data()[0].items;
866
867 for (i, c) in items.iter().enumerate() {
868 let e = make_entry(c);
869 node_add(&mut n, c.as_bytes(), e, BTreeMap::new());
870
871 for &d in items.iter().take(i) {
872 let r = node_lookup(&mut n, d.as_bytes()).unwrap();
873 assert_eq!(r, Some(&make_entry(d)));
874 }
875 }
876 }
877
878 fn run_add_and_lookup_node(items: &[&str]) {
879 let mut n = Node::default();
880
881 for (i, c) in items.iter().enumerate() {
882 let e = make_entry(c);
883 node_add(&mut n, c.as_bytes(), e, BTreeMap::new());
884
885 for &d in items.iter().take(i) {
886 let node = node_lookup_node(&mut n, d.as_bytes()).unwrap();
887 assert!(node.is_value());
888 assert_eq!(node.entry(), Some(&make_entry(d)));
889 }
890 }
891 }
892
893 #[test]
894 fn add_and_lookup_node_a() {
895 run_add_and_lookup_node(&test_case_data()[0].items);
896 }
897
898 #[test]
899 fn add_and_lookup_node_simple() {
900 run_add_and_lookup_node(&test_case_data()[1].items);
901 }
902
903 #[test]
904 fn add_and_lookup_node_nested_value() {
905 run_add_and_lookup_node(&test_case_data()[2].items);
906 }
907
908 #[test]
909 fn add_and_lookup_node_nested_prefix() {
910 run_add_and_lookup_node(&test_case_data()[3].items);
911 }
912
913 #[test]
914 fn add_and_lookup_node_conflicting_path() {
915 run_add_and_lookup_node(&test_case_data()[4].items);
916 }
917
918 #[test]
919 fn add_and_lookup_node_spa_website() {
920 run_add_and_lookup_node(&test_case_data()[5].items);
921 }
922
923 fn run_add_and_lookup_with_load_save(items: &[&str]) {
924 let mut n = Node::default();
925
926 for c in items {
927 let e = make_entry(c);
928 node_add(&mut n, c.as_bytes(), e, BTreeMap::new());
929 }
930
931 let store = MemoryStore::<{ DEFAULT_BODY_SIZE }>::new();
932 n.save(&store).unwrap();
933
934 let mut n2: Node = Node::from_reference(n.reference.unwrap());
935
936 for &d in items {
937 let node = n2.lookup_node(d.as_bytes(), &store).unwrap();
938 assert!(node.is_value());
939 assert_eq!(node.entry(), Some(&make_entry(d)));
940 }
941 }
942
943 #[test]
944 fn add_and_lookup_with_load_save_a() {
945 run_add_and_lookup_with_load_save(&test_case_data()[0].items);
946 }
947
948 #[test]
949 fn add_and_lookup_with_load_save_simple() {
950 run_add_and_lookup_with_load_save(&test_case_data()[1].items);
951 }
952
953 #[test]
954 fn add_and_lookup_with_load_save_nested_value() {
955 run_add_and_lookup_with_load_save(&test_case_data()[2].items);
956 }
957
958 #[test]
959 fn add_and_lookup_with_load_save_nested_prefix() {
960 run_add_and_lookup_with_load_save(&test_case_data()[3].items);
961 }
962
963 #[test]
964 fn add_and_lookup_with_load_save_conflicting_path() {
965 run_add_and_lookup_with_load_save(&test_case_data()[4].items);
966 }
967
968 #[test]
969 fn add_and_lookup_with_load_save_spa_website() {
970 run_add_and_lookup_with_load_save(&test_case_data()[5].items);
971 }
972
973 fn run_remove(tc: RemoveTestCase) {
974 let mut n = Node::default();
975
976 for (i, c) in tc.items.iter().enumerate() {
977 let e = make_entry(&c.path);
978 node_add(&mut n, c.path.as_bytes(), e, c.metadata.clone());
979
980 for item in tc.items.iter().take(i) {
981 let r = node_lookup(&mut n, item.path.as_bytes()).unwrap();
982 assert_eq!(r, Some(&make_entry(&item.path)));
983 }
984 }
985
986 for c in &tc.remove {
987 node_remove(&mut n, c.as_bytes()).unwrap();
988 assert!(node_lookup(&mut n, c.as_bytes()).is_err());
989 }
990 }
991
992 #[test]
993 fn remove_simple() {
994 run_remove(remove_test_case_data()[0].clone());
995 }
996
997 #[test]
998 fn remove_nested_prefix() {
999 run_remove(remove_test_case_data()[1].clone());
1000 }
1001
1002 fn run_has_prefix(tc: HasPrefixTestCase) {
1003 let mut n = Node::default();
1004
1005 for c in &tc.paths {
1006 let e = make_entry(c);
1007 node_add(&mut n, c.as_bytes(), e, BTreeMap::default());
1008 }
1009
1010 for (i, test_prefix) in tc.test_paths.iter().enumerate() {
1011 assert_eq!(
1012 node_has_prefix(&mut n, test_prefix.as_bytes()).unwrap(),
1013 tc.should_exist[i],
1014 );
1015 }
1016 }
1017
1018 #[test]
1019 fn has_prefix_simple() {
1020 run_has_prefix(has_prefix_test_case_data()[0].clone());
1021 }
1022
1023 #[test]
1024 fn has_prefix_nested_single() {
1025 run_has_prefix(has_prefix_test_case_data()[1].clone());
1026 }
1027
1028 fn run_persist_remove(tc: RemoveTestCase) {
1031 let store = MemoryStore::<{ DEFAULT_BODY_SIZE }>::new();
1032
1033 let mut n = Node::default();
1035 for c in &tc.items {
1036 let e = make_entry(&c.path);
1037 n.add(c.path.as_bytes(), Some(e), c.metadata.clone(), &store)
1038 .unwrap();
1039 }
1040 n.save(&store).unwrap();
1041 let ref_ = n.reference.unwrap();
1042
1043 let mut nn: Node = Node::from_reference(ref_);
1045 for path in &tc.remove {
1046 nn.remove(path.as_bytes(), &store).unwrap();
1047 }
1048 nn.save(&store).unwrap();
1049 let ref2 = nn.reference.unwrap();
1050
1051 let mut nnn: Node = Node::from_reference(ref2);
1053 for path in &tc.remove {
1054 let result = nnn.lookup_node(path.as_bytes(), &store);
1055 assert!(
1056 result.is_err(),
1057 "expected removed path '{path}' to be not found"
1058 );
1059 }
1060 }
1061
1062 #[test]
1063 fn persist_remove_simple() {
1064 run_persist_remove(remove_test_case_data()[0].clone());
1065 }
1066
1067 #[test]
1068 fn persist_remove_nested_prefix() {
1069 run_persist_remove(remove_test_case_data()[1].clone());
1070 }
1071
1072 fn make_entry_bytes(s: &[u8]) -> ChunkAddress {
1073 let mut buf = [0u8; 32];
1074 let start = 32 - s.len();
1075 buf[start..].copy_from_slice(s);
1076 ChunkAddress::from(buf)
1077 }
1078
1079 #[test]
1080 fn walk_visits_all_nodes() {
1081 let mut root = Node::default();
1082
1083 let paths = &["index.html", "img/1.png", "img/2.png", "robots.txt"];
1084 for &p in paths {
1085 let entry = make_entry_bytes(p.as_bytes());
1086 node_add(&mut root, p.as_bytes(), entry, BTreeMap::new());
1087 }
1088
1089 let mut visited: Vec<(Vec<u8>, bool)> = Vec::new();
1090 node_walk(&mut root, &mut |path, node| {
1091 visited.push((path.to_vec(), node.is_value()));
1092 Ok(())
1093 })
1094 .unwrap();
1095
1096 for &p in paths {
1097 assert!(
1098 visited
1099 .iter()
1100 .any(|(vp, is_val)| vp == p.as_bytes() && *is_val),
1101 "path {p} not visited as value"
1102 );
1103 }
1104 }
1105
1106 #[test]
1107 fn walk_node_exact_order() {
1108 let to_add: &[&[u8]] = &[
1109 b"index.html.backup",
1110 b"index.html",
1111 b"img/test/oho.png",
1112 b"img/test/old/test.png.backup",
1113 b"img/test/old/test.png",
1114 b"img/2.png",
1115 b"img/1.png",
1116 b"robots.txt",
1117 ];
1118
1119 let expected: &[&[u8]] = &[
1120 b"",
1121 b"i",
1122 b"img/",
1123 b"img/1.png",
1124 b"img/2.png",
1125 b"img/test/o",
1126 b"img/test/oho.png",
1127 b"img/test/old/test.png",
1128 b"img/test/old/test.png.backup",
1129 b"index.html",
1130 b"index.html.backup",
1131 b"robots.txt",
1132 ];
1133
1134 let mut n = Node::default();
1135 for &path in to_add {
1136 let entry = make_entry_bytes(path);
1137 node_add(&mut n, path, entry, BTreeMap::new());
1138 }
1139
1140 let mut walked: Vec<Vec<u8>> = Vec::new();
1141 node_walk_node(&mut n, b"", &mut |path, _node| {
1142 walked.push(path.to_vec());
1143 Ok(())
1144 })
1145 .unwrap();
1146
1147 assert_eq!(
1148 walked.len(),
1149 expected.len(),
1150 "expected {} nodes, got {}",
1151 expected.len(),
1152 walked.len()
1153 );
1154
1155 for (i, (got, &want)) in walked.iter().zip(expected.iter()).enumerate() {
1156 assert_eq!(
1157 got.as_slice(),
1158 want,
1159 "walk step {i}: expected {:?}, got {:?}",
1160 core::str::from_utf8(want).unwrap_or("<non-utf8>"),
1161 core::str::from_utf8(got).unwrap_or("<non-utf8>"),
1162 );
1163 }
1164 }
1165
1166 #[test]
1167 fn walk_node_from_subtree() {
1168 let to_add: &[&[u8]] = &[b"index.html", b"img/1.png", b"img/2.png", b"robots.txt"];
1169
1170 let mut n = Node::default();
1171 for &path in to_add {
1172 let entry = make_entry_bytes(path);
1173 node_add(&mut n, path, entry, BTreeMap::new());
1174 }
1175
1176 let mut walked: Vec<Vec<u8>> = Vec::new();
1177 node_walk_node(&mut n, b"img/", &mut |path, _node| {
1178 walked.push(path.to_vec());
1179 Ok(())
1180 })
1181 .unwrap();
1182
1183 assert!(walked.iter().any(|p| p == b"img/1.png"));
1184 assert!(walked.iter().any(|p| p == b"img/2.png"));
1185 assert!(!walked.iter().any(|p| p == b"index.html"));
1186 assert!(!walked.iter().any(|p| p == b"robots.txt"));
1187 }
1188
1189 #[test]
1190 fn walk_node_exact_order_with_load_save() {
1191 let to_add: &[&[u8]] = &[
1192 b"index.html.backup",
1193 b"index.html",
1194 b"img/test/oho.png",
1195 b"img/test/old/test.png.backup",
1196 b"img/test/old/test.png",
1197 b"img/2.png",
1198 b"img/1.png",
1199 b"robots.txt",
1200 ];
1201
1202 let expected: &[&[u8]] = &[
1203 b"",
1204 b"i",
1205 b"img/",
1206 b"img/1.png",
1207 b"img/2.png",
1208 b"img/test/o",
1209 b"img/test/oho.png",
1210 b"img/test/old/test.png",
1211 b"img/test/old/test.png.backup",
1212 b"index.html",
1213 b"index.html.backup",
1214 b"robots.txt",
1215 ];
1216
1217 let mut n = Node::default();
1218 for &path in to_add {
1219 let entry = make_entry_bytes(path);
1220 node_add(&mut n, path, entry, BTreeMap::new());
1221 }
1222
1223 let store = MemoryStore::<{ DEFAULT_BODY_SIZE }>::new();
1224 n.save(&store).unwrap();
1225
1226 let mut n2: Node = Node::from_reference(n.reference.unwrap());
1227
1228 let mut walked: Vec<Vec<u8>> = Vec::new();
1229 n2.walk_from(b"", &store, &mut |path: &[u8], _node: &Node| {
1230 walked.push(path.to_vec());
1231 Ok(())
1232 })
1233 .unwrap();
1234
1235 assert_eq!(
1236 walked.len(),
1237 expected.len(),
1238 "expected {} nodes, got {}",
1239 expected.len(),
1240 walked.len()
1241 );
1242
1243 for (i, (got, &want)) in walked.iter().zip(expected.iter()).enumerate() {
1244 assert_eq!(
1245 got.as_slice(),
1246 want,
1247 "walk step {i}: expected {:?}, got {:?}",
1248 core::str::from_utf8(want).unwrap_or("<non-utf8>"),
1249 core::str::from_utf8(got).unwrap_or("<non-utf8>"),
1250 );
1251 }
1252 }
1253}