1use rustc_hash::FxHashMap;
9
10#[derive(Debug, Clone)]
12pub struct Entry {
13 pub rel: Box<str>,
15 pub root: u16,
17 pub is_dir: bool,
19 pub size: u64,
21}
22
23#[derive(Debug, Default)]
25pub struct IndexStore {
26 entries: Vec<Entry>,
27 by_key: FxHashMap<(u16, Box<str>), u32>,
29}
30
31impl IndexStore {
32 pub fn new() -> Self {
33 Self::default()
34 }
35
36 pub fn len(&self) -> usize {
37 self.entries.len()
38 }
39
40 pub fn is_empty(&self) -> bool {
41 self.entries.is_empty()
42 }
43
44 pub fn entries(&self) -> &[Entry] {
45 &self.entries
46 }
47
48 pub fn insert(&mut self, root: u16, rel: Box<str>, is_dir: bool, size: u64) -> bool {
50 let key = (root, rel.clone());
51 if let Some(&idx) = self.by_key.get(&key) {
52 let e = &mut self.entries[idx as usize];
53 e.is_dir = is_dir;
54 e.size = size;
55 return false;
56 }
57 let idx = self.entries.len() as u32;
58 self.entries.push(Entry {
59 rel,
60 root,
61 is_dir,
62 size,
63 });
64 self.by_key.insert(key, idx);
65 true
66 }
67
68 pub fn remove(&mut self, root: u16, rel: &str) -> bool {
70 let key = (root, Box::<str>::from(rel));
73 let Some(idx) = self.by_key.remove(&key) else {
74 return false;
75 };
76 let idx = idx as usize;
77 self.entries.swap_remove(idx);
78 if idx < self.entries.len() {
79 let moved = &self.entries[idx];
80 let moved_key = (moved.root, moved.rel.clone());
81 self.by_key.insert(moved_key, idx as u32);
82 }
83 true
84 }
85
86 pub fn remove_tree(&mut self, root: u16, rel: &str) -> usize {
88 let prefix = format!("{rel}/");
89 let mut removed = 0usize;
90 let victims: Vec<Box<str>> = self
92 .entries
93 .iter()
94 .filter(|e| e.root == root && (&*e.rel == rel || e.rel.starts_with(&prefix)))
95 .map(|e| e.rel.clone())
96 .collect();
97 for rel in victims {
98 if self.remove(root, &rel) {
99 removed += 1;
100 }
101 }
102 removed
103 }
104
105 pub fn clear(&mut self) {
106 self.entries.clear();
107 self.by_key.clear();
108 }
109
110 pub fn browse(&self, root: u16, rel_dir: &str) -> Vec<&Entry> {
113 let prefix = if rel_dir.is_empty() {
114 String::new()
115 } else {
116 format!("{}/", rel_dir.trim_end_matches('/'))
117 };
118 let mut out: Vec<&Entry> = self
119 .entries
120 .iter()
121 .filter(|e| {
122 e.root == root && e.rel.starts_with(prefix.as_str()) && {
123 let rest = &e.rel[prefix.len()..];
124 !rest.is_empty() && !rest.contains('/')
125 }
126 })
127 .collect();
128 out.sort_by(|a, b| {
129 b.is_dir
130 .cmp(&a.is_dir)
131 .then_with(|| a.rel.to_ascii_lowercase().cmp(&b.rel.to_ascii_lowercase()))
132 });
133 out
134 }
135
136 pub fn iter_root(&self, root: u16) -> impl Iterator<Item = &Entry> {
138 self.entries.iter().filter(move |e| e.root == root)
139 }
140}
141
142#[cfg(test)]
143#[path = "store_tests.rs"]
144mod tests;