sos_search/
search.rs

1//! Search provides an in-memory index for secret meta data.
2use crate::{Error, Result};
3use probly_search::{score::bm25, Index, QueryResult};
4use serde::{Deserialize, Serialize};
5use sos_backend::AccessPoint;
6use sos_core::{crypto::AccessKey, VaultId};
7use sos_vault::{
8    secret::{Secret, SecretId, SecretMeta, SecretRef, SecretType},
9    SecretAccess, Summary, Vault,
10};
11use std::{
12    borrow::Cow,
13    collections::{btree_map::Values, BTreeMap, HashMap, HashSet},
14    sync::Arc,
15};
16use tokio::sync::RwLock;
17use unicode_segmentation::UnicodeSegmentation;
18use url::Url;
19
20/// Create a set of ngrams of the given size.
21#[doc(hidden)]
22pub fn ngram_slice(s: &str, n: usize) -> HashSet<&str> {
23    let mut items: HashSet<&str> = HashSet::new();
24    let graphemes: Vec<usize> =
25        s.grapheme_indices(true).map(|v| v.0).collect();
26    for (index, offset) in graphemes.iter().enumerate() {
27        if let Some(end_offset) = graphemes.get(index + n) {
28            items.insert(&s[*offset..*end_offset]);
29        } else {
30            let mut end_offset = offset;
31            for i in 1..n {
32                if let Some(end) = graphemes.get(index + i) {
33                    end_offset = end;
34                }
35            }
36            if end_offset > offset {
37                let val = &s[*offset..*end_offset];
38                items.insert(val);
39            }
40        }
41    }
42    items
43}
44
45/// Key for meta data documents.
46#[derive(Debug, Clone, Eq, Ord, PartialEq, PartialOrd, Serialize)]
47pub struct DocumentKey(String, VaultId, SecretId);
48
49// Index tokenizer.
50fn tokenizer(s: &str) -> Vec<Cow<'_, str>> {
51    let words = s.split(' ').collect::<HashSet<_>>();
52
53    let ngram2 = ngram_slice(s, 2);
54    let ngram3 = ngram_slice(s, 3);
55    let ngram4 = ngram_slice(s, 4);
56    let ngram5 = ngram_slice(s, 5);
57    let ngram: HashSet<&str> = ngram2.union(&ngram3).map(|s| &**s).collect();
58    let ngram: HashSet<&str> = ngram.union(&ngram4).map(|s| &**s).collect();
59    let ngram: HashSet<&str> = ngram.union(&ngram5).map(|s| &**s).collect();
60
61    let mut tokens: Vec<Cow<str>> = Vec::new();
62    for token in ngram.union(&words) {
63        tokens.push(Cow::Owned(token.to_lowercase()))
64    }
65    tokens
66}
67
68// Query tokenizer.
69fn query_tokenizer(s: &str) -> Vec<Cow<'_, str>> {
70    s.split(' ')
71        .map(|s| s.to_lowercase())
72        .map(Cow::Owned)
73        .collect::<Vec<_>>()
74}
75
76// Label
77fn label_extract(d: &Document) -> Vec<&str> {
78    vec![d.meta().label()]
79}
80
81// Tags
82fn tags_extract(d: &Document) -> Vec<&str> {
83    d.meta().tags().iter().map(|s| &s[..]).collect()
84}
85
86// Comment
87fn comment_extract(d: &Document) -> Vec<&str> {
88    if let Some(comment) = d.extra().comment() {
89        vec![comment]
90    } else {
91        vec![""]
92    }
93}
94
95// Website
96fn website_extract(d: &Document) -> Vec<&str> {
97    if let Some(websites) = d.extra().websites() {
98        websites
99        // vec![]
100    } else {
101        vec![]
102    }
103}
104
105/// Count of documents by vault identitier and secret kind.
106#[derive(Default, Debug, Clone)]
107pub struct DocumentCount {
108    /// Count number of documents in each vault.
109    vaults: HashMap<VaultId, usize>,
110    /// Count number of documents across all vaults by secret kind.
111    kinds: HashMap<u8, usize>,
112    /// Map tags to counts.
113    tags: HashMap<String, usize>,
114    /// Count number of favorites.
115    favorites: usize,
116    /// Identifier for an archive vault.
117    ///
118    /// Documents in an archive vault are omitted from the kind counts
119    /// so that client implementations can show correct counts when
120    /// ignoring archived items from lists.
121    archive: Option<VaultId>,
122}
123
124impl DocumentCount {
125    /// Create a new document count.
126    pub fn new(archive: Option<VaultId>) -> Self {
127        Self {
128            vaults: Default::default(),
129            kinds: Default::default(),
130            tags: Default::default(),
131            favorites: Default::default(),
132            archive,
133        }
134    }
135
136    /// Set the identifier for an archive vault.
137    pub fn set_archive_id(&mut self, archive: Option<VaultId>) {
138        self.archive = archive;
139    }
140
141    /// Get the counts by vault.
142    pub fn vaults(&self) -> &HashMap<VaultId, usize> {
143        &self.vaults
144    }
145
146    /// Get the counts by kinds.
147    pub fn kinds(&self) -> &HashMap<u8, usize> {
148        &self.kinds
149    }
150
151    /// Get the counts by tags.
152    pub fn tags(&self) -> &HashMap<String, usize> {
153        &self.tags
154    }
155
156    /// Get the count of favorites.
157    pub fn favorites(&self) -> usize {
158        self.favorites
159    }
160
161    /// Determine if a document vault identifier matches
162    /// an archive vault.
163    fn is_archived(&self, folder_id: &VaultId) -> bool {
164        if let Some(archive) = &self.archive {
165            return folder_id == archive;
166        }
167        false
168    }
169
170    /// Document was removed, update the count.
171    fn remove(
172        &mut self,
173        folder_id: VaultId,
174        mut options: Option<(u8, HashSet<String>, bool)>,
175    ) {
176        self.vaults
177            .entry(folder_id)
178            .and_modify(|counter| {
179                if *counter > 0 {
180                    *counter -= 1;
181                }
182            })
183            .or_insert(0);
184
185        if let Some((kind, tags, favorite)) = options.take() {
186            if !self.is_archived(&folder_id) {
187                self.kinds
188                    .entry(kind)
189                    .and_modify(|counter| {
190                        if *counter > 0 {
191                            *counter -= 1;
192                        }
193                    })
194                    .or_insert(0);
195            }
196
197            for tag in &tags {
198                self.tags
199                    .entry(tag.to_owned())
200                    .and_modify(|counter| {
201                        if *counter > 0 {
202                            *counter -= 1;
203                        }
204                    })
205                    .or_insert(0);
206
207                // Clean up the tag when count reaches zero
208                let value = self.tags.get(tag).unwrap_or(&0);
209                if *value == 0 {
210                    self.tags.remove(tag);
211                }
212            }
213
214            if favorite && self.favorites > 0 {
215                self.favorites -= 1;
216            }
217        }
218    }
219
220    /// Document was added, update the count.
221    fn add(
222        &mut self,
223        folder_id: VaultId,
224        kind: u8,
225        tags: &HashSet<String>,
226        favorite: bool,
227    ) {
228        self.vaults
229            .entry(folder_id)
230            .and_modify(|counter| *counter += 1)
231            .or_insert(1);
232
233        if !self.is_archived(&folder_id) {
234            self.kinds
235                .entry(kind)
236                .and_modify(|counter| *counter += 1)
237                .or_insert(1);
238        }
239        for tag in tags {
240            self.tags
241                .entry(tag.to_owned())
242                .and_modify(|counter| *counter += 1)
243                .or_insert(1);
244        }
245
246        if favorite {
247            self.favorites += 1;
248        }
249    }
250}
251
252/// Collection of statistics for the search index.
253#[derive(Debug)]
254pub struct IndexStatistics {
255    /// Document counts.
256    count: DocumentCount,
257}
258
259impl IndexStatistics {
260    /// Create new statistics.
261    pub fn new(archive: Option<VaultId>) -> Self {
262        Self {
263            count: DocumentCount::new(archive),
264        }
265    }
266
267    /// Set the identifier for an archive vault.
268    pub fn set_archive_id(&mut self, archive: Option<VaultId>) {
269        self.count.set_archive_id(archive);
270    }
271
272    /// Get the statistics count.
273    pub fn count(&self) -> &DocumentCount {
274        &self.count
275    }
276}
277
278/// Additional fields that can exposed via search results
279/// that are extracted from the secret data but safe to
280/// be exposed.
281#[typeshare::typeshare]
282#[derive(Default, Debug, Serialize, Deserialize, Clone)]
283#[serde(rename_all = "camelCase")]
284pub struct ExtraFields {
285    /// Comment about a secret.
286    pub comment: Option<String>,
287    /// Contact type for contact secrets.
288    pub contact_type: Option<vcard4::property::Kind>,
289    /// Collection of websites.
290    pub websites: Option<Vec<String>>,
291}
292
293impl From<&Secret> for ExtraFields {
294    fn from(value: &Secret) -> Self {
295        let mut extra = ExtraFields {
296            comment: value.user_data().comment().map(|c| c.to_owned()),
297            websites: value
298                .websites()
299                .map(|w| w.into_iter().map(|u| u.to_string()).collect()),
300            ..Default::default()
301        };
302        if let Secret::Contact { vcard, .. } = value {
303            extra.contact_type = vcard
304                .kind
305                .as_ref()
306                .map(|p| p.value.clone())
307                .or(Some(vcard4::property::Kind::Individual));
308        }
309        extra
310    }
311}
312
313impl ExtraFields {
314    /// Optional comment.
315    pub fn comment(&self) -> Option<&str> {
316        self.comment.as_ref().map(|c| &c[..])
317    }
318
319    /// Optional websites.
320    pub fn websites(&self) -> Option<Vec<&str>> {
321        self.websites
322            .as_ref()
323            .map(|u| u.into_iter().map(|u| &u[..]).collect())
324    }
325}
326
327/// Document that can be indexed.
328#[typeshare::typeshare]
329#[derive(Debug, Serialize, Deserialize, Clone)]
330#[serde(rename_all = "camelCase")]
331pub struct Document {
332    /// Folder identifier.
333    pub folder_id: VaultId,
334    /// Secret identifier.
335    pub secret_id: SecretId,
336    /// Secret meta data.
337    pub meta: SecretMeta,
338    /// Extra fields for the document.
339    pub extra: ExtraFields,
340}
341
342impl Document {
343    /// Get the vault identifier.
344    pub fn folder_id(&self) -> &VaultId {
345        &self.folder_id
346    }
347
348    /// Get the secret identifier.
349    pub fn id(&self) -> &SecretId {
350        &self.secret_id
351    }
352
353    /// Get the secret meta data.
354    pub fn meta(&self) -> &SecretMeta {
355        &self.meta
356    }
357
358    /// Get the extra fields.
359    pub fn extra(&self) -> &ExtraFields {
360        &self.extra
361    }
362}
363
364/// Exposes access to a search index of meta data.
365pub struct SearchIndex {
366    index: Index<(VaultId, SecretId)>,
367    documents: BTreeMap<DocumentKey, Document>,
368    statistics: IndexStatistics,
369}
370
371impl Default for SearchIndex {
372    fn default() -> Self {
373        Self::new()
374    }
375}
376
377impl SearchIndex {
378    /// Create a new search index.
379    pub fn new() -> Self {
380        // Create index with N fields
381        let index = Index::<(VaultId, SecretId)>::new(4);
382        Self {
383            index,
384            documents: Default::default(),
385            statistics: IndexStatistics::new(None),
386        }
387    }
388
389    /// Set the identifier for an archive vault.
390    pub fn set_archive_id(&mut self, archive: Option<VaultId>) {
391        self.statistics.set_archive_id(archive);
392    }
393
394    /// Search index statistics.
395    pub fn statistics(&self) -> &IndexStatistics {
396        &self.statistics
397    }
398
399    /// Collection of documents.
400    pub fn documents(&self) -> &BTreeMap<DocumentKey, Document> {
401        &self.documents
402    }
403
404    /// List of the document values.
405    pub fn values(&self) -> Vec<&Document> {
406        self.documents.values().collect::<Vec<_>>()
407    }
408
409    /// Iterator over all the values.
410    pub fn values_iter(&self) -> Values<'_, DocumentKey, Document> {
411        self.documents.values()
412    }
413
414    /// Number of documents in the index.
415    pub fn len(&self) -> usize {
416        self.documents.len()
417    }
418
419    /// Determine if the search index is empty.
420    pub fn is_empty(&self) -> bool {
421        self.len() == 0
422    }
423
424    /// Find document by label.
425    ///
426    // FIXME: use _name suffix to be consistent with attachment handling
427    pub fn find_by_label<'a>(
428        &'a self,
429        folder_id: &VaultId,
430        label: &str,
431        id: Option<&SecretId>,
432    ) -> Option<&'a Document> {
433        self.documents
434            .values()
435            .filter(|d| {
436                if let Some(id) = id {
437                    id != d.id()
438                } else {
439                    true
440                }
441            })
442            .find(|d| d.folder_id() == folder_id && d.meta().label() == label)
443    }
444
445    /// Find document by label in any vault.
446    pub fn find_by_label_any<'a>(
447        &'a self,
448        label: &str,
449        id: Option<&SecretId>,
450        case_insensitive: bool,
451    ) -> Option<&'a Document> {
452        self.documents
453            .values()
454            .filter(|d| {
455                if let Some(id) = id {
456                    id != d.id()
457                } else {
458                    true
459                }
460            })
461            .find(|d| {
462                if case_insensitive {
463                    d.meta().label().to_lowercase() == label.to_lowercase()
464                } else {
465                    d.meta().label() == label
466                }
467            })
468    }
469
470    /// Find all documents with the given label ignoring
471    /// a particular identifier.
472    ///
473    // FIXME: use _name suffix to be consistent with attachment handling
474    pub fn find_all_by_label<'a>(
475        &'a self,
476        label: &str,
477        id: Option<&SecretId>,
478    ) -> Vec<&'a Document> {
479        self.documents
480            .iter()
481            .filter(|(k, v)| {
482                if let Some(id) = id {
483                    if id == &k.1 {
484                        false
485                    } else {
486                        v.meta().label() == label
487                    }
488                } else {
489                    v.meta().label() == label
490                }
491            })
492            .map(|(_, v)| v)
493            .collect::<Vec<_>>()
494    }
495
496    /// Find document by id.
497    pub fn find_by_id<'a>(
498        &'a self,
499        folder_id: &VaultId,
500        id: &SecretId,
501    ) -> Option<&'a Document> {
502        self.documents
503            .values()
504            .find(|d| d.folder_id() == folder_id && d.id() == id)
505    }
506
507    /// Find secret meta by uuid or label.
508    ///
509    // FIXME: use _name suffix to be consistent with attachment handling
510    pub fn find_by_uuid_or_label<'a>(
511        &'a self,
512        folder_id: &VaultId,
513        target: &SecretRef,
514    ) -> Option<&'a Document> {
515        match target {
516            SecretRef::Id(id) => self.find_by_id(folder_id, id),
517            SecretRef::Name(name) => {
518                self.find_by_label(folder_id, name, None)
519            }
520        }
521    }
522
523    /// Prepare a document for insertion.
524    ///
525    /// If a document with the given identifiers exists
526    /// then no document is created.
527    pub fn prepare(
528        &self,
529        folder_id: &VaultId,
530        id: &SecretId,
531        meta: &SecretMeta,
532        secret: &Secret,
533    ) -> Option<(DocumentKey, Document)> {
534        // Prevent duplicates
535        if self.find_by_id(folder_id, id).is_none() {
536            let doc = Document {
537                folder_id: *folder_id,
538                secret_id: *id,
539                meta: meta.clone(),
540                extra: secret.into(),
541            };
542
543            // Listing key includes the identifier so that
544            // secrets with the same label do not overwrite each other
545            let key = DocumentKey(
546                doc.meta().label().to_lowercase(),
547                *folder_id,
548                *id,
549            );
550
551            Some((key, doc))
552        } else {
553            None
554        }
555    }
556
557    /// Commit a prepared key and document.
558    pub fn commit(&mut self, doc: Option<(DocumentKey, Document)>) {
559        // Prevent duplicates
560        if let Some((key, doc)) = doc {
561            let exists = self.documents.get(&key).is_some();
562            let doc = self.documents.entry(key).or_insert(doc);
563            if !exists {
564                self.index.add_document(
565                    &[
566                        label_extract,
567                        tags_extract,
568                        comment_extract,
569                        website_extract,
570                    ],
571                    tokenizer,
572                    (doc.folder_id, doc.secret_id),
573                    doc,
574                );
575
576                self.statistics.count.add(
577                    doc.folder_id,
578                    doc.meta().kind().into(),
579                    doc.meta().tags(),
580                    doc.meta().favorite(),
581                );
582            }
583        }
584    }
585
586    /// Add a document to the index.
587    pub fn add(
588        &mut self,
589        folder_id: &VaultId,
590        id: &SecretId,
591        meta: &SecretMeta,
592        secret: &Secret,
593    ) {
594        self.commit(self.prepare(folder_id, id, meta, secret));
595    }
596
597    /// Update a document in the index.
598    pub fn update(
599        &mut self,
600        folder_id: &VaultId,
601        id: &SecretId,
602        meta: &SecretMeta,
603        secret: &Secret,
604    ) {
605        self.remove(folder_id, id);
606        self.add(folder_id, id, meta, secret);
607    }
608
609    /// Add the meta data from the entries in a folder
610    /// to this search index.
611    pub async fn add_folder(&mut self, folder: &AccessPoint) -> Result<()> {
612        let vault = folder.vault();
613        for id in vault.keys() {
614            let (meta, secret, _) = folder
615                .read_secret(id)
616                .await?
617                .ok_or_else(|| Error::NoSecretId(*folder.id(), *id))?;
618            self.add(folder.id(), id, &meta, &secret);
619        }
620        Ok(())
621    }
622
623    /// Remove the meta data from the entries in a folder.
624    pub async fn remove_folder(
625        &mut self,
626        folder: &AccessPoint,
627    ) -> Result<()> {
628        let vault = folder.vault();
629        for id in vault.keys() {
630            self.remove(folder.id(), id);
631        }
632        Ok(())
633    }
634
635    /// Remove and vacuum a document from the index.
636    pub fn remove(&mut self, folder_id: &VaultId, id: &SecretId) {
637        let key = self
638            .documents
639            .keys()
640            .find(|key| &key.1 == folder_id && &key.2 == id)
641            .cloned();
642        let doc_info = if let Some(key) = &key {
643            let doc = self.documents.remove(key);
644            doc.map(|doc| {
645                let kind: u8 = doc.meta().kind().into();
646                (kind, doc.meta().tags().clone(), doc.meta().favorite())
647            })
648        } else {
649            None
650        };
651
652        self.index.remove_document((*folder_id, *id));
653        // Vacuum to remove completely
654        self.index.vacuum();
655
656        self.statistics.count.remove(*folder_id, doc_info);
657    }
658
659    /// Remove all the documents for a given vault identifier from the index.
660    pub fn remove_vault(&mut self, folder_id: &VaultId) {
661        let keys: Vec<DocumentKey> = self
662            .documents
663            .keys()
664            .filter(|k| &k.1 == folder_id)
665            .cloned()
666            .collect();
667        for key in keys {
668            self.remove(&key.1, &key.2);
669            self.documents.remove(&key);
670        }
671    }
672
673    /// Remove all documents from the index.
674    ///
675    /// This should be called before creating a new index using
676    /// the same search index instance.
677    pub fn remove_all(&mut self) {
678        let keys: Vec<DocumentKey> = self.documents.keys().cloned().collect();
679        for key in keys {
680            self.remove(&key.1, &key.2);
681            self.documents.remove(&key);
682        }
683    }
684
685    /// Query the index.
686    pub fn query(
687        &self,
688        needle: &str,
689    ) -> Vec<QueryResult<(VaultId, SecretId)>> {
690        self.index.query(
691            needle,
692            &mut bm25::new(),
693            query_tokenizer,
694            &[1., 1., 1., 1.],
695        )
696    }
697
698    /// Query the index and map each result to the corresponding document.
699    ///
700    /// If a corresponding document could not be located the search result
701    /// will be ignored.
702    pub fn query_map(
703        &self,
704        needle: &str,
705        predicate: impl Fn(&Document) -> bool,
706    ) -> Vec<&Document> {
707        let results = self.query(needle);
708        results
709            .into_iter()
710            .filter_map(|r| {
711                self.find_by_id(&r.key.0, &r.key.1)
712                    .filter(|&doc| predicate(doc))
713            })
714            .collect::<Vec<_>>()
715    }
716}
717
718/// Account statistics derived from the search index.
719#[derive(Default, Debug, Serialize, Deserialize)]
720pub struct AccountStatistics {
721    /// Number of documents in the search index.
722    pub documents: usize,
723    /// Folder counts.
724    pub folders: Vec<(Summary, usize)>,
725    /// Tag counts.
726    pub tags: HashMap<String, usize>,
727    /// Types.
728    pub types: HashMap<SecretType, usize>,
729    /// Number of favorites.
730    pub favorites: usize,
731}
732
733/// Modify and query the search index for an account.
734pub struct AccountSearch {
735    /// Search index.
736    pub search_index: Arc<RwLock<SearchIndex>>,
737}
738
739impl Default for AccountSearch {
740    fn default() -> Self {
741        Self::new()
742    }
743}
744
745impl AccountSearch {
746    /// Create a new user search index.
747    pub fn new() -> Self {
748        Self {
749            search_index: Arc::new(RwLock::new(SearchIndex::new())),
750        }
751    }
752
753    /// Get a reference to the search index.
754    #[doc(hidden)]
755    pub fn search(&self) -> Arc<RwLock<SearchIndex>> {
756        Arc::clone(&self.search_index)
757    }
758
759    /// Clear the entire search index.
760    pub async fn clear(&mut self) {
761        let mut writer = self.search_index.write().await;
762        writer.remove_all();
763    }
764
765    /// Add a folder which must be unlocked.
766    pub async fn add_folder(&self, folder: &AccessPoint) -> Result<()> {
767        let mut index = self.search_index.write().await;
768        index.add_folder(folder).await
769    }
770
771    /// Remove a folder from the search index.
772    pub async fn remove_folder(&self, folder_id: &VaultId) {
773        // Clean entries from the search index
774        let mut writer = self.search_index.write().await;
775        writer.remove_vault(folder_id);
776    }
777
778    /// Add a vault to the search index.
779    pub async fn add_vault(
780        &self,
781        vault: Vault,
782        key: &AccessKey,
783    ) -> Result<()> {
784        let mut index = self.search_index.write().await;
785        let mut keeper = AccessPoint::from_vault(vault);
786        keeper.unlock(key).await?;
787        index.add_folder(&keeper).await?;
788        keeper.lock();
789        Ok(())
790    }
791
792    /// Get the search index document count statistics.
793    pub async fn document_count(&self) -> DocumentCount {
794        let reader = self.search_index.read().await;
795        reader.statistics().count().clone()
796    }
797
798    /// Determine if a document exists in a folder.
799    pub async fn document_exists(
800        &self,
801        folder_id: &VaultId,
802        label: &str,
803        id: Option<&SecretId>,
804    ) -> bool {
805        let reader = self.search_index.read().await;
806        reader.find_by_label(folder_id, label, id).is_some()
807    }
808
809    /// Query with document views.
810    pub async fn query_view(
811        &self,
812        views: &[DocumentView],
813        archive: Option<&ArchiveFilter>,
814    ) -> Result<Vec<Document>> {
815        let index_reader = self.search_index.read().await;
816        let mut docs = Vec::with_capacity(index_reader.len());
817        for doc in index_reader.values_iter() {
818            for view in views {
819                if view.test(doc, archive) {
820                    docs.push(doc.clone());
821                }
822            }
823        }
824        Ok(docs)
825    }
826
827    /// Query the search index.
828    pub async fn query_map(
829        &self,
830        query: &str,
831        filter: QueryFilter,
832    ) -> Result<Vec<Document>> {
833        let index_reader = self.search_index.read().await;
834        let mut docs = Vec::new();
835        let tags: HashSet<_> = filter.tags.iter().cloned().collect();
836        let predicate = self.query_predicate(filter, tags);
837        if !query.is_empty() {
838            for doc in index_reader.query_map(query, predicate) {
839                docs.push(doc.clone());
840            }
841        } else {
842            for doc in index_reader.values_iter() {
843                if predicate(doc) {
844                    docs.push(doc.clone());
845                }
846            }
847        }
848        Ok(docs)
849    }
850
851    fn query_predicate(
852        &self,
853        filter: QueryFilter,
854        tags: HashSet<String>,
855    ) -> impl Fn(&Document) -> bool {
856        move |doc| {
857            let tag_match = filter.tags.is_empty() || {
858                !tags
859                    .intersection(doc.meta().tags())
860                    .collect::<HashSet<_>>()
861                    .is_empty()
862            };
863
864            let folder_id = doc.folder_id();
865            let folder_match = filter.folders.is_empty()
866                || filter.folders.contains(folder_id);
867
868            let type_match = filter.types.is_empty()
869                || filter.types.contains(doc.meta().kind());
870
871            tag_match && folder_match && type_match
872        }
873    }
874}
875
876/// View of documents in the search index.
877#[typeshare::typeshare]
878#[derive(Debug, Clone, Serialize, Deserialize)]
879#[serde(rename_all = "camelCase", tag = "kind", content = "body")]
880pub enum DocumentView {
881    /// View all documents in the search index.
882    All {
883        /// List of secret types to ignore.
884        #[serde(rename = "ignoredTypes")]
885        ignored_types: Option<Vec<SecretType>>,
886    },
887    /// View all the documents for a folder.
888    Vault(VaultId),
889    /// View documents across all vaults by type identifier.
890    TypeId(SecretType),
891    /// View for all favorites.
892    Favorites,
893    /// View documents that have one or more tags.
894    Tags(Vec<String>),
895    /// Contacts of the given types.
896    Contact {
897        /// Contact types to include in the results.
898        ///
899        /// If no types are specified all types are included.
900        include_types: Option<Vec<vcard4::property::Kind>>,
901    },
902    /// Documents with the specific identifiers.
903    Documents {
904        /// Vault identifier.
905        #[serde(rename = "folderId")]
906        folder_id: VaultId,
907        /// Secret identifiers.
908        identifiers: Vec<SecretId>,
909    },
910    /// Secrets with the associated websites.
911    Websites {
912        /// Secrets that match the given target URLs.
913        matches: Option<Vec<Url>>,
914        /// Exact match requires that the match targets and
915        /// websites are exactly equal. Otherwise, comparison
916        /// is performed using the URL origin.
917        exact: bool,
918    },
919}
920
921impl Default for DocumentView {
922    fn default() -> Self {
923        Self::All {
924            ignored_types: None,
925        }
926    }
927}
928
929impl DocumentView {
930    /// Test this view against a search result document.
931    pub fn test(
932        &self,
933        doc: &Document,
934        archive: Option<&ArchiveFilter>,
935    ) -> bool {
936        if let Some(filter) = archive {
937            if !filter.include_documents && doc.folder_id() == &filter.id {
938                return false;
939            }
940        }
941        match self {
942            DocumentView::All { ignored_types } => {
943                if let Some(ignored_types) = ignored_types {
944                    return !ignored_types.contains(doc.meta().kind());
945                }
946                true
947            }
948            DocumentView::Vault(folder_id) => doc.folder_id() == folder_id,
949            DocumentView::TypeId(type_id) => doc.meta().kind() == type_id,
950            DocumentView::Favorites => doc.meta().favorite(),
951            DocumentView::Tags(tags) => {
952                let tags: HashSet<_> = tags.iter().cloned().collect();
953                !tags
954                    .intersection(doc.meta().tags())
955                    .collect::<HashSet<_>>()
956                    .is_empty()
957            }
958            DocumentView::Contact { include_types } => {
959                if doc.meta().kind() == &SecretType::Contact {
960                    if let Some(include_types) = include_types {
961                        if let Some(contact_type) = &doc.extra().contact_type
962                        {
963                            let contact_type: vcard4::property::Kind =
964                                contact_type.clone();
965                            return include_types.contains(&contact_type);
966                        } else {
967                            return false;
968                        }
969                    }
970                    return true;
971                }
972                false
973            }
974            DocumentView::Documents {
975                folder_id,
976                identifiers,
977            } => {
978                doc.folder_id() == folder_id && identifiers.contains(doc.id())
979            }
980            DocumentView::Websites { matches, exact } => {
981                if let Some(sites) = doc.extra().websites() {
982                    if sites.is_empty() {
983                        false
984                    } else {
985                        if let Some(targets) = matches {
986                            // Search index stores as string but
987                            // we need to compare as URLs
988                            let mut urls: Vec<Url> =
989                                Vec::with_capacity(sites.len());
990                            for site in sites {
991                                match site.parse() {
992                                    Ok(url) => urls.push(url),
993                                    Err(e) => {
994                                        tracing::warn!(
995                                            error = %e,
996                                            "search::url_parse");
997                                    }
998                                }
999                            }
1000
1001                            if *exact {
1002                                for url in targets {
1003                                    if urls.contains(url) {
1004                                        return true;
1005                                    }
1006                                }
1007                                false
1008                            } else {
1009                                for url in targets {
1010                                    for site in &urls {
1011                                        if url.origin() == site.origin() {
1012                                            return true;
1013                                        }
1014                                    }
1015                                }
1016                                false
1017                            }
1018                        } else {
1019                            // No target matches but has some
1020                            // associated websites so include in the view
1021                            true
1022                        }
1023                    }
1024                } else {
1025                    false
1026                }
1027            }
1028        }
1029    }
1030}
1031
1032/// Filter for a search query.
1033#[typeshare::typeshare]
1034#[derive(Default, Debug, Clone, Serialize, Deserialize)]
1035pub struct QueryFilter {
1036    /// List of tags.
1037    pub tags: Vec<String>,
1038    /// List of vault identifiers.
1039    pub folders: Vec<VaultId>,
1040    /// List of type identifiers.
1041    pub types: Vec<SecretType>,
1042}
1043
1044/// Filter for archived documents.
1045#[typeshare::typeshare]
1046#[derive(Debug, Clone, Serialize, Deserialize)]
1047#[serde(rename_all = "camelCase")]
1048pub struct ArchiveFilter {
1049    /// Identifier of the archive vault.
1050    pub id: VaultId,
1051    /// Whether to include archived documents.
1052    pub include_documents: bool,
1053}