1
use std::{sync::Arc, time::Duration};
2

            
3
use ashpd::WindowIdentifier;
4
#[cfg(feature = "async-std")]
5
use async_lock::RwLock;
6
use futures_util::{Stream, StreamExt};
7
#[cfg(feature = "tokio")]
8
use tokio::sync::RwLock;
9
use zbus::zvariant::{ObjectPath, OwnedObjectPath};
10

            
11
use super::{Algorithm, Error, Item, api};
12
use crate::{AsAttributes, Key, Secret};
13

            
14
/// A collection allows to store and retrieve items.
15
///
16
/// The collection can be either in a locked or unlocked state, use
17
/// [`Collection::lock`] or [`Collection::unlock`] to lock or unlock it.
18
///
19
/// Using [`Collection::search_items`] or [`Collection::items`] will return no
20
/// items if the collection is locked.
21
///
22
/// **Note**
23
///
24
/// If the collection is deleted using [`Collection::delete`] any future usage
25
/// of it API will fail with [`Error::Deleted`].
26
#[derive(Debug)]
27
pub struct Collection<'a> {
28
    inner: Arc<api::Collection<'a>>,
29
    service: Arc<api::Service<'a>>,
30
    session: Arc<api::Session<'a>>,
31
    algorithm: Algorithm,
32
    /// Defines whether the Collection has been deleted or not
33
    available: RwLock<bool>,
34
    aes_key: Option<Arc<Key>>,
35
}
36

            
37
impl<'a> Collection<'a> {
38
2
    pub(crate) fn new(
39
        service: Arc<api::Service<'a>>,
40
        session: Arc<api::Session<'a>>,
41
        algorithm: Algorithm,
42
        collection: api::Collection<'a>,
43
        aes_key: Option<Arc<Key>>,
44
    ) -> Collection<'a> {
45
        Self {
46
4
            inner: Arc::new(collection),
47
            session,
48
            service,
49
            algorithm,
50
2
            available: RwLock::new(true),
51
            aes_key,
52
        }
53
    }
54

            
55
8
    pub(crate) async fn is_available(&self) -> bool {
56
4
        *self.available.read().await
57
    }
58

            
59
    /// Retrieve the list of available [`Item`] in the collection.
60
8
    pub async fn items(&self) -> Result<Vec<Item<'a>>, Error> {
61
6
        if !self.is_available().await {
62
            Err(Error::Deleted)
63
        } else {
64
10
            Ok(self
65
                .inner
66
2
                .items()
67
8
                .await?
68
2
                .into_iter()
69
6
                .map(|item| self.new_item(item))
70
2
                .collect::<Vec<_>>())
71
        }
72
    }
73

            
74
    /// The collection label.
75
8
    pub async fn label(&self) -> Result<String, Error> {
76
4
        if !self.is_available().await {
77
            Err(Error::Deleted)
78
        } else {
79
6
            self.inner.label().await
80
        }
81
    }
82

            
83
    /// Set the collection label.
84
8
    pub async fn set_label(&self, label: &str) -> Result<(), Error> {
85
4
        if !self.is_available().await {
86
            Err(Error::Deleted)
87
        } else {
88
6
            self.inner.set_label(label).await
89
        }
90
    }
91

            
92
    /// Get whether the collection is locked.
93
    #[doc(alias = "Locked")]
94
    pub async fn is_locked(&self) -> Result<bool, Error> {
95
        if !self.is_available().await {
96
            Err(Error::Deleted)
97
        } else {
98
            self.inner.is_locked().await
99
        }
100
    }
101

            
102
    /// The UNIX time when the collection was created.
103
    pub async fn created(&self) -> Result<Duration, Error> {
104
        if !self.is_available().await {
105
            Err(Error::Deleted)
106
        } else {
107
            self.inner.created().await
108
        }
109
    }
110

            
111
    /// The UNIX time when the collection was modified.
112
    pub async fn modified(&self) -> Result<Duration, Error> {
113
        if !self.is_available().await {
114
            Err(Error::Deleted)
115
        } else {
116
            self.inner.modified().await
117
        }
118
    }
119

            
120
    /// Search for items based on their attributes.
121
6
    pub async fn search_items(
122
        &self,
123
        attributes: &impl AsAttributes,
124
    ) -> Result<Vec<Item<'a>>, Error> {
125
18
        if !self.is_available().await {
126
            Err(Error::Deleted)
127
        } else {
128
18
            let items = self.inner.search_items(attributes).await?;
129
12
            Ok(items
130
6
                .into_iter()
131
18
                .map(|item| self.new_item(item))
132
6
                .collect::<Vec<_>>())
133
        }
134
    }
135

            
136
    /// Create a new item on the collection
137
    ///
138
    /// # Arguments
139
    ///
140
    /// * `label` - A user visible label of the item.
141
    /// * `attributes` - A map of key/value attributes, used to find the item
142
    ///   later.
143
    /// * `secret` - The secret to store.
144
    /// * `replace` - Whether to replace the value if the `attributes` matches
145
    ///   an existing `secret`.
146
10
    pub async fn create_item(
147
        &self,
148
        label: &str,
149
        attributes: &impl AsAttributes,
150
        secret: impl Into<Secret>,
151
        replace: bool,
152
        window_id: Option<WindowIdentifier>,
153
    ) -> Result<Item<'a>, Error> {
154
20
        if !self.is_available().await {
155
            Err(Error::Deleted)
156
        } else {
157
10
            let secret = match self.algorithm {
158
6
                Algorithm::Plain => api::DBusSecret::new(Arc::clone(&self.session), secret),
159
                Algorithm::Encrypted => api::DBusSecret::new_encrypted(
160
16
                    Arc::clone(&self.session),
161
12
                    secret,
162
20
                    self.aes_key.as_ref().unwrap(),
163
                )?,
164
            };
165
40
            let item = self
166
                .inner
167
10
                .create_item(label, attributes, &secret, replace, window_id)
168
40
                .await?;
169

            
170
20
            Ok(self.new_item(item))
171
        }
172
    }
173

            
174
    /// Unlock the collection.
175
    pub async fn unlock(&self, window_id: Option<WindowIdentifier>) -> Result<(), Error> {
176
        if !self.is_available().await {
177
            Err(Error::Deleted)
178
        } else {
179
            self.service
180
                .unlock(&[self.inner.inner().path()], window_id)
181
                .await?;
182
            Ok(())
183
        }
184
    }
185

            
186
    /// Lock the collection.
187
    pub async fn lock(&self, window_id: Option<WindowIdentifier>) -> Result<(), Error> {
188
        if !self.is_available().await {
189
            Err(Error::Deleted)
190
        } else {
191
            self.service
192
                .lock(&[self.inner.inner().path()], window_id)
193
                .await?;
194
            Ok(())
195
        }
196
    }
197

            
198
    /// Delete the collection.
199
    pub async fn delete(&self, window_id: Option<WindowIdentifier>) -> Result<(), Error> {
200
        if !self.is_available().await {
201
            Err(Error::Deleted)
202
        } else {
203
            self.inner.delete(window_id).await?;
204
            *self.available.write().await = false;
205
            Ok(())
206
        }
207
    }
208

            
209
    /// Returns collection path
210
    pub fn path(&self) -> &ObjectPath<'_> {
211
        self.inner.inner().path()
212
    }
213

            
214
    /// Stream yielding when new items get created
215
    pub async fn receive_item_created(&self) -> Result<impl Stream<Item = Item<'a>> + '_, Error> {
216
        Ok(self
217
            .inner
218
            .receive_item_created()
219
            .await?
220
            .map(|item| self.new_item(item)))
221
    }
222

            
223
    /// Stream yielding when existing items get changed
224
    pub async fn receive_item_changed(&self) -> Result<impl Stream<Item = Item<'a>> + '_, Error> {
225
        Ok(self
226
            .inner
227
            .receive_item_changed()
228
            .await?
229
            .map(|item| self.new_item(item)))
230
    }
231

            
232
    /// Stream yielding when existing items get deleted
233
    pub async fn receive_item_deleted(&self) -> Result<impl Stream<Item = OwnedObjectPath>, Error> {
234
        self.inner.receive_item_deleted().await
235
    }
236

            
237
    // Get public `Item`` from `api::Item`
238
2
    fn new_item(&self, item: api::Item<'a>) -> Item<'a> {
239
        Item::new(
240
4
            Arc::clone(&self.service),
241
4
            Arc::clone(&self.session),
242
2
            self.algorithm,
243
2
            item,
244
2
            self.aes_key.clone(), // Cheap clone, it is an Arc,
245
        )
246
    }
247
}
248

            
249
#[cfg(test)]
250
#[cfg(feature = "tokio")]
251
mod tests {
252
    use crate::dbus::Service;
253

            
254
    async fn create_item(service: Service<'_>, encrypted: bool) {
255
        let attributes = if encrypted {
256
            &[("type", "encrypted-type-test")]
257
        } else {
258
            &[("type", "plain-type-test")]
259
        };
260
        let secret = crate::Secret::text("a password");
261

            
262
        let collection = service.default_collection().await.unwrap();
263
        let n_search_items = collection.search_items(&attributes).await.unwrap().len();
264

            
265
        let item = collection
266
            .create_item("A secret", &attributes, secret.clone(), true, None)
267
            .await
268
            .unwrap();
269

            
270
        assert_eq!(item.secret().await.unwrap(), secret);
271
        assert_eq!(
272
            item.attributes().await.unwrap().get("type").unwrap(),
273
            attributes[0].1,
274
        );
275

            
276
        assert_eq!(
277
            collection.search_items(&attributes).await.unwrap().len(),
278
            n_search_items + 1
279
        );
280

            
281
        item.delete(None).await.unwrap();
282

            
283
        assert_eq!(
284
            collection.search_items(&attributes).await.unwrap().len(),
285
            n_search_items
286
        );
287
    }
288

            
289
    #[tokio::test]
290
    async fn create_plain_item() {
291
        let service = Service::plain().await.unwrap();
292
        create_item(service, false).await;
293
    }
294

            
295
    #[tokio::test]
296
    async fn create_encrypted_item() {
297
        let service = Service::encrypted().await.unwrap();
298
        create_item(service, true).await;
299
    }
300

            
301
    #[tokio::test]
302
    async fn attribute_search_patterns() {
303
        let service = Service::plain().await.unwrap();
304
        let collection = service.default_collection().await.unwrap();
305

            
306
        let secret = crate::Secret::text("search test");
307

            
308
        // Create items with unique test attributes
309
        let item1 = collection
310
            .create_item(
311
                "Pattern Test 1",
312
                &[("test-pattern", "pattern-test-a"), ("category", "group1")],
313
                secret.clone(),
314
                true,
315
                None,
316
            )
317
            .await
318
            .unwrap();
319

            
320
        let item2 = collection
321
            .create_item(
322
                "Pattern Test 2",
323
                &[("test-pattern", "pattern-test-a"), ("category", "group2")],
324
                secret.clone(),
325
                true,
326
                None,
327
            )
328
            .await
329
            .unwrap();
330

            
331
        let item3 = collection
332
            .create_item(
333
                "Pattern Test 3",
334
                &[("test-pattern", "pattern-test-b"), ("category", "group1")],
335
                secret.clone(),
336
                true,
337
                None,
338
            )
339
            .await
340
            .unwrap();
341

            
342
        // Search by test-pattern - should find items with pattern-test-a
343
        let pattern_a_items = collection
344
            .search_items(&[("test-pattern", "pattern-test-a")])
345
            .await
346
            .unwrap();
347
        let found_paths: std::collections::HashSet<_> =
348
            pattern_a_items.iter().map(|item| item.path()).collect();
349
        assert!(found_paths.contains(item1.path()));
350
        assert!(found_paths.contains(item2.path()));
351

            
352
        // Search by category - should find items in group1
353
        let group1_items = collection
354
            .search_items(&[("category", "group1")])
355
            .await
356
            .unwrap();
357
        let found_group1_paths: std::collections::HashSet<_> =
358
            group1_items.iter().map(|item| item.path()).collect();
359
        assert!(found_group1_paths.contains(item1.path()));
360
        assert!(found_group1_paths.contains(item3.path()));
361

            
362
        // Search by both attributes - should find only item1
363
        let specific_items = collection
364
            .search_items(&[("test-pattern", "pattern-test-a"), ("category", "group1")])
365
            .await
366
            .unwrap();
367
        let found_specific_paths: std::collections::HashSet<_> =
368
            specific_items.iter().map(|item| item.path()).collect();
369
        assert!(found_specific_paths.contains(item1.path()));
370

            
371
        item1.delete(None).await.unwrap();
372
        item2.delete(None).await.unwrap();
373
        item3.delete(None).await.unwrap();
374
    }
375

            
376
    #[tokio::test]
377
    async fn items() {
378
        let service = Service::plain().await.unwrap();
379
        let collection = service.default_collection().await.unwrap();
380

            
381
        let secret = crate::Secret::text("items test");
382

            
383
        // Create some test items with unique attributes
384
        let item1 = collection
385
            .create_item(
386
                "Test Item 1",
387
                &[("test", "items-test-1"), ("unique", "test-1")],
388
                secret.clone(),
389
                true,
390
                None,
391
            )
392
            .await
393
            .unwrap();
394

            
395
        let item2 = collection
396
            .create_item(
397
                "Test Item 2",
398
                &[("test", "items-test-2"), ("unique", "test-2")],
399
                secret.clone(),
400
                true,
401
                None,
402
            )
403
            .await
404
            .unwrap();
405

            
406
        // Get all items and verify our items are included by path
407
        let all_items = collection.items().await.unwrap();
408
        let item_paths: std::collections::HashSet<_> =
409
            all_items.iter().map(|item| item.path()).collect();
410

            
411
        assert!(item_paths.contains(item1.path()));
412
        assert!(item_paths.contains(item2.path()));
413

            
414
        // Clean up
415
        item1.delete(None).await.unwrap();
416
        item2.delete(None).await.unwrap();
417
    }
418

            
419
    #[tokio::test]
420
    async fn label_mutation() {
421
        let service = Service::plain().await.unwrap();
422
        let collection = service.session_collection().await.unwrap();
423

            
424
        let initial_label = collection.label().await.unwrap();
425

            
426
        collection.set_label("Updated Label").await.unwrap();
427
        assert_eq!(collection.label().await.unwrap(), "Updated Label");
428
        assert_ne!(collection.label().await.unwrap(), initial_label);
429

            
430
        // Restore original label
431
        collection.set_label(&initial_label).await.unwrap();
432
        assert_eq!(collection.label().await.unwrap(), initial_label);
433
    }
434
}