lb_rs/model/
lazy.rs

1use crate::model::access_info::UserAccessMode;
2use crate::model::crypto::AESKey;
3use crate::model::errors::{LbErrKind, LbResult};
4use crate::model::file_like::FileLike;
5use crate::model::file_metadata::{FileType, Owner};
6use crate::model::staged::StagedTree;
7use crate::model::symkey;
8use crate::model::tree_like::{TreeLike, TreeLikeMut};
9use crate::service::keychain::Keychain;
10use serde::{Deserialize, Serialize};
11use std::collections::{HashMap, HashSet};
12use uuid::Uuid;
13
14#[derive(Clone, Debug)]
15pub struct LazyTree<T: TreeLike> {
16    pub tree: T,
17    pub name: HashMap<Uuid, String>,
18    pub implicit_deleted: HashMap<Uuid, bool>,
19    pub linked_by: HashMap<Uuid, Uuid>,
20    pub children: HashMap<Uuid, HashSet<Uuid>>,
21}
22
23impl<T: TreeLike> LazyTree<T> {
24    pub fn new(tree: T) -> Self {
25        Self {
26            name: HashMap::new(),
27            implicit_deleted: HashMap::new(),
28            children: HashMap::new(),
29            linked_by: HashMap::new(),
30            tree,
31        }
32    }
33}
34
35impl<T: TreeLike> LazyTree<T> {
36    pub fn access_mode(&self, owner: Owner, id: &Uuid) -> LbResult<Option<UserAccessMode>> {
37        let mut file = self.find(id)?;
38        let mut max_access_mode = None;
39        let mut visited_ids = vec![];
40        while !visited_ids.contains(file.id()) {
41            visited_ids.push(*file.id());
42            let access_mode = file.access_mode(&owner);
43            if access_mode > max_access_mode {
44                max_access_mode = access_mode;
45            }
46            if file.parent() == file.id() {
47                break; // root
48            } else if let Some(parent) = self.maybe_find(file.parent()) {
49                file = parent
50            } else {
51                break; // share root
52            }
53        }
54        Ok(max_access_mode)
55    }
56
57    pub fn in_pending_share(&mut self, id: &Uuid) -> LbResult<bool> {
58        let mut id = *id;
59        loop {
60            if self.find(&id)?.parent() == self.find(&id)?.id() {
61                return Ok(false); // root
62            } else if let Some(link) = self.linked_by(&id)? {
63                id = link;
64            } else if self.maybe_find(self.find(&id)?.parent()).is_some() {
65                id = *self.find(&id)?.parent();
66            } else {
67                return Ok(true); // share root
68            }
69        }
70    }
71
72    pub fn all_children(&mut self) -> LbResult<&HashMap<Uuid, HashSet<Uuid>>> {
73        if self.children.is_empty() {
74            let mut all_children: HashMap<Uuid, HashSet<Uuid>> = HashMap::new();
75            for file in self.all_files()? {
76                if !file.is_root() {
77                    let mut children = all_children.remove(file.parent()).unwrap_or_default();
78                    children.insert(*file.id());
79                    all_children.insert(*file.parent(), children);
80                }
81            }
82            self.children = all_children;
83        }
84
85        Ok(&self.children)
86    }
87
88    pub fn calculate_deleted(&mut self, id: &Uuid) -> LbResult<bool> {
89        let (visited_ids, deleted) = {
90            let mut file = self.find(id)?;
91            let mut visited_ids = vec![];
92            let mut deleted = false;
93
94            while !file.is_root()
95                && self.maybe_find(file.parent()).is_some()
96                && !visited_ids.contains(file.id())
97            {
98                visited_ids.push(*file.id());
99                if let Some(&implicit) = self.implicit_deleted.get(file.id()) {
100                    deleted = implicit;
101                    break;
102                }
103
104                if file.explicitly_deleted() {
105                    deleted = true;
106                    break;
107                }
108
109                file = match self.maybe_find_parent(file) {
110                    Some(file) => file,
111                    None => {
112                        if !file.user_access_keys().is_empty() {
113                            break;
114                        } else {
115                            return Err(LbErrKind::FileParentNonexistent)?;
116                        }
117                    }
118                }
119            }
120
121            (visited_ids, deleted)
122        };
123
124        for id in visited_ids {
125            self.implicit_deleted.insert(id, deleted);
126        }
127
128        Ok(deleted)
129    }
130
131    pub fn decrypt_key(&mut self, id: &Uuid, keychain: &Keychain) -> LbResult<AESKey> {
132        let mut file_id = *self.find(id)?.id();
133        let mut visited_ids = vec![];
134
135        loop {
136            if keychain.contains_aes_key(&file_id)? {
137                break;
138            }
139
140            let my_pk = keychain.get_pk()?;
141
142            let maybe_file_key = if let Some(user_access) = self
143                .find(&file_id)?
144                .user_access_keys()
145                .iter()
146                .find(|access| access.encrypted_for == my_pk)
147            {
148                Some(user_access.decrypt(keychain.get_account()?)?)
149            } else {
150                None
151            };
152            if let Some(file_key) = maybe_file_key {
153                keychain.insert_aes_key(file_id, file_key)?;
154                break;
155            }
156
157            visited_ids.push(file_id);
158            file_id = *self.find_parent(self.find(&file_id)?)?.id();
159        }
160
161        for id in visited_ids.iter().rev() {
162            let decrypted_key = {
163                let file = self.find(id)?;
164                let parent = self.find_parent(file)?;
165                let parent_key =
166                    keychain
167                        .get_aes_key(parent.id())?
168                        .ok_or(LbErrKind::Unexpected(
169                            "parent key should have been populated by prior routine".to_string(),
170                        ))?;
171                let encrypted_key = file.folder_access_key();
172                symkey::decrypt(&parent_key, encrypted_key)?
173            };
174            keychain.insert_aes_key(*id, decrypted_key)?;
175        }
176
177        Ok(keychain.get_aes_key(id)?.ok_or(LbErrKind::Unexpected(
178            "parent key should have been populated by prior routine (2)".to_string(),
179        ))?)
180    }
181
182    pub fn name(&mut self, id: &Uuid, keychain: &Keychain) -> LbResult<String> {
183        if let Some(name) = self.name.get(id) {
184            return Ok(name.clone());
185        }
186        let key = self.decrypt_key(id, keychain)?;
187        let name = self.find(id)?.secret_name().to_string(&key)?;
188        self.name.insert(*id, name.clone());
189        Ok(name)
190    }
191
192    pub fn name_using_links(&mut self, id: &Uuid, keychain: &Keychain) -> LbResult<String> {
193        let id = if let Some(link) = self.linked_by(id)? { link } else { *id };
194        self.name(&id, keychain)
195    }
196
197    pub fn parent_using_links(&mut self, id: &Uuid) -> LbResult<Uuid> {
198        let id = if let Some(link) = self.linked_by(id)? { link } else { *id };
199        Ok(*self.find(&id)?.parent())
200    }
201
202    pub fn linked_by(&mut self, id: &Uuid) -> LbResult<Option<Uuid>> {
203        if self.linked_by.is_empty() {
204            for link_id in self.ids() {
205                if let FileType::Link { target } = self.find(&link_id)?.file_type() {
206                    if !self.calculate_deleted(&link_id)? {
207                        self.linked_by.insert(target, link_id);
208                    }
209                }
210            }
211        }
212
213        Ok(self.linked_by.get(id).copied())
214    }
215
216    /// Returns ids of files whose parent is the argument. Does not include the argument.
217    /// TODO could consider returning a reference to the underlying cached value
218    pub fn children(&mut self, id: &Uuid) -> LbResult<HashSet<Uuid>> {
219        // Check cache
220        if let Some(children) = self.children.get(id) {
221            return Ok(children.clone());
222        }
223
224        // Confirm file exists
225        let file = self.find(id)?;
226        if !file.is_folder() {
227            return Ok(HashSet::default());
228        }
229
230        // Populate cache
231        self.all_children()?;
232
233        // Return value from cache
234        if let Some(children) = self.children.get(id) {
235            return Ok(children.clone());
236        }
237
238        Ok(HashSet::new())
239    }
240
241    // todo: cache?
242    pub fn children_using_links(&mut self, id: &Uuid) -> LbResult<HashSet<Uuid>> {
243        let mut children = HashSet::new();
244        for child in self.children(id)? {
245            if let FileType::Link { target } = self.find(&child)?.file_type() {
246                if !self.calculate_deleted(&child)? {
247                    children.insert(target);
248                }
249            } else {
250                children.insert(child);
251            }
252        }
253
254        Ok(children)
255    }
256
257    /// Returns ids of files for which the argument is an ancestor—the files' children, recursively. Does not include the argument.
258    /// This function tolerates cycles.
259    pub fn descendants(&mut self, id: &Uuid) -> LbResult<HashSet<Uuid>> {
260        // todo: caching?
261        let mut result = HashSet::new();
262        let mut to_process = vec![*id];
263        let mut i = 0;
264        while i < to_process.len() {
265            let new_descendents = self
266                .children(&to_process[i])?
267                .into_iter()
268                .filter(|f| !result.contains(f))
269                .collect::<Vec<Uuid>>();
270            // TODO could consider optimizing by not exploring documents
271            to_process.extend(new_descendents.iter());
272            result.extend(new_descendents.into_iter());
273            i += 1;
274        }
275        Ok(result)
276    }
277
278    pub fn descendants_using_links(&mut self, id: &Uuid) -> LbResult<HashSet<Uuid>> {
279        // todo: caching?
280        let mut result = HashSet::new();
281        let mut to_process = vec![*id];
282        let mut i = 0;
283        while i < to_process.len() {
284            let new_descendents = self
285                .children_using_links(&to_process[i])?
286                .into_iter()
287                .filter(|f| !result.contains(f))
288                .collect::<Vec<Uuid>>();
289            // TODO could consider optimizing by not exploring documents
290            to_process.extend(new_descendents.iter());
291            result.extend(new_descendents.into_iter());
292            i += 1;
293        }
294        Ok(result)
295    }
296
297    // todo: move to TreeLike
298    /// Returns ids of files for which the argument is a descendent—the files' parent, recursively. Does not include the argument.
299    /// This function tolerates cycles.
300    pub fn ancestors(&self, id: &Uuid) -> LbResult<HashSet<Uuid>> {
301        let mut result = HashSet::new();
302        let mut current_file = self.find(id)?;
303        while !current_file.is_root()
304            && self.maybe_find(current_file.parent()).is_some()
305            && !result.contains(current_file.parent())
306        {
307            result.insert(*current_file.parent());
308            current_file = self.find_parent(current_file)?;
309        }
310        Ok(result)
311    }
312
313    pub fn stage<T2: TreeLikeMut<F = T::F>>(self, staged: T2) -> LazyTree<StagedTree<T, T2>> {
314        // todo: optimize by performing minimal updates on self caches
315        LazyTree::<StagedTree<T, T2>> {
316            tree: StagedTree::new(self.tree, staged),
317            name: HashMap::new(),
318            implicit_deleted: HashMap::new(),
319            children: HashMap::new(),
320            linked_by: HashMap::new(),
321        }
322    }
323
324    // todo: this is dead code
325    pub fn stage_removals(self, removed: HashSet<Uuid>) -> LazyTree<StagedTree<T, Option<T::F>>> {
326        // todo: optimize by performing minimal updates on self caches
327        LazyTree::<StagedTree<T, Option<T::F>>> {
328            tree: StagedTree::removal(self.tree, removed),
329            name: HashMap::new(),
330            implicit_deleted: HashMap::new(),
331            children: HashMap::new(),
332            linked_by: HashMap::new(),
333        }
334    }
335
336    // todo: optimize
337    pub fn assert_names_decryptable(&mut self, keychain: &Keychain) -> LbResult<()> {
338        for id in self.ids() {
339            if self.name(&id, keychain).is_err() {
340                return Err(LbErrKind::Validation(ValidationFailure::NonDecryptableFileName(id)))?;
341            }
342        }
343        Ok(())
344    }
345}
346
347pub type Stage1<Base, Local> = StagedTree<Base, Local>;
348pub type LazyStaged1<Base, Local> = LazyTree<Stage1<Base, Local>>;
349pub type Stage2<Base, Local, Staged> = StagedTree<StagedTree<Base, Local>, Staged>;
350pub type LazyStage2<Base, Local, Staged> = LazyTree<Stage2<Base, Local, Staged>>;
351
352#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Eq)]
353pub enum ValidationFailure {
354    Orphan(Uuid),
355
356    /// A folder was moved into itself
357    Cycle(HashSet<Uuid>),
358
359    /// A filename is not available
360    PathConflict(HashSet<Uuid>),
361
362    /// This filename is too long
363    FileNameTooLong(Uuid),
364
365    /// A link or document was treated as a folder
366    NonFolderWithChildren(Uuid),
367
368    /// You cannot have a link to a file you own
369    OwnedLink(Uuid),
370
371    /// You cannot have a link that points to a nonexistent file
372    BrokenLink(Uuid),
373
374    /// You cannot have multiple links to the same file
375    DuplicateLink {
376        target: Uuid,
377    },
378
379    /// You cannot have a link inside a shared folder
380    SharedLink {
381        link: Uuid,
382        shared_ancestor: Uuid,
383    },
384
385    FileWithDifferentOwnerParent(Uuid),
386    NonDecryptableFileName(Uuid),
387    DeletedFileUpdated(Uuid),
388}
389
390impl<T> LazyTree<T>
391where
392    T: TreeLikeMut,
393{
394    pub fn stage_and_promote<S: TreeLikeMut<F = T::F>>(&mut self, mut staged: S) -> LbResult<()> {
395        for id in staged.ids() {
396            if let Some(removed) = staged.remove(id)? {
397                self.tree.insert(removed)?;
398            }
399        }
400        // todo: incremental cache update
401        self.name = HashMap::new();
402        self.implicit_deleted = HashMap::new();
403        self.children = HashMap::new();
404        self.linked_by = HashMap::new();
405        Ok(())
406    }
407
408    pub fn stage_validate_and_promote<S: TreeLikeMut<F = T::F>>(
409        &mut self, mut staged: S, owner: Owner,
410    ) -> LbResult<()> {
411        StagedTree::new(&self.tree, &mut staged)
412            .to_lazy()
413            .validate(owner)?;
414        self.stage_and_promote(staged)?;
415        Ok(())
416    }
417
418    // todo: this is dead code
419    pub fn stage_removals_and_promote(&mut self, removed: HashSet<Uuid>) -> LbResult<()> {
420        for id in removed {
421            self.tree.remove(id)?;
422        }
423        // todo: incremental cache update
424        self.name = HashMap::new();
425        self.implicit_deleted = HashMap::new();
426        self.children = HashMap::new();
427        self.linked_by = HashMap::new();
428        Ok(())
429    }
430}
431
432impl<Base, Staged> LazyStaged1<Base, Staged>
433where
434    Base: TreeLikeMut,
435    Staged: TreeLikeMut<F = Base::F>,
436{
437    // todo: incrementalism
438    pub fn promote(self) -> LbResult<LazyTree<Base>> {
439        let mut staged = self.tree.staged;
440        let mut base = self.tree.base;
441        for id in staged.ids() {
442            if let Some(removed) = staged.remove(id)? {
443                base.insert(removed)?;
444            }
445        }
446        for id in self.tree.removed {
447            base.remove(id)?;
448        }
449
450        Ok(LazyTree {
451            tree: base,
452            name: HashMap::new(),
453            implicit_deleted: HashMap::new(),
454            children: HashMap::new(),
455            linked_by: HashMap::new(),
456        })
457    }
458}
459
460impl<Base, Staged> LazyStaged1<Base, Staged>
461where
462    Base: TreeLike,
463    Staged: TreeLikeMut<F = Base::F>,
464{
465    // todo: incrementalism
466    pub fn unstage(self) -> (LazyTree<Base>, Staged) {
467        (
468            LazyTree {
469                tree: self.tree.base,
470                name: HashMap::new(),
471                implicit_deleted: HashMap::new(),
472                children: HashMap::new(),
473                linked_by: HashMap::new(),
474            },
475            self.tree.staged,
476        )
477    }
478}
479
480impl<T: TreeLike> TreeLike for LazyTree<T> {
481    type F = T::F;
482
483    fn ids(&self) -> Vec<Uuid> {
484        self.tree.ids()
485    }
486
487    fn maybe_find(&self, id: &Uuid) -> Option<&Self::F> {
488        self.tree.maybe_find(id)
489    }
490}