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

            
3
#[cfg(feature = "async-std")]
4
use async_lock::RwLock;
5
#[cfg(feature = "tokio")]
6
use tokio::sync::RwLock;
7

            
8
use crate::{AsAttributes, Result, Secret, dbus, file};
9

            
10
/// A [Secret Service](crate::dbus) or [file](crate::file) backed keyring
11
/// implementation.
12
///
13
/// It will automatically use the file backend if the application is sandboxed
14
/// and otherwise falls back to the DBus service using it [default
15
/// collection](crate::dbus::Service::default_collection).
16
///
17
/// The File backend requires a [`org.freedesktop.portal.Secret`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Secret.html) implementation
18
/// to retrieve the key that will be used to encrypt the backend file.
19
#[derive(Debug)]
20
pub enum Keyring {
21
    #[doc(hidden)]
22
    File(Arc<RwLock<Option<file::Keyring>>>, Secret),
23
    #[doc(hidden)]
24
    DBus(dbus::Collection),
25
}
26

            
27
impl Keyring {
28
    /// Create a new instance of the Keyring.
29
    ///
30
    /// Auto-detects whether the application is sandboxed and uses the
31
    /// appropriate backend (file backend for sandboxed apps, D-Bus service
32
    /// for host apps). Falls back to D-Bus if the secret portal is not
33
    /// available.
34
    pub async fn new() -> Result<Self> {
35
        if ashpd::is_sandboxed() {
36
            match Self::sandboxed().await {
37
                Ok(keyring) => Ok(keyring),
38
                // Fallback to host keyring if portal is not available
39
                Err(crate::Error::File(file::Error::Portal(ashpd::Error::PortalNotFound(_)))) => {
40
                    #[cfg(feature = "tracing")]
41
                    tracing::debug!(
42
                        "org.freedesktop.portal.Secrets is not available, falling back to the Secret Service backend"
43
                    );
44
                    Self::host().await
45
                }
46
                Err(e) => Err(e),
47
            }
48
        } else {
49
            Self::host().await
50
        }
51
    }
52

            
53
    /// Use the file backend with secret portal (for sandboxed apps).
54
    pub async fn sandboxed() -> Result<Self> {
55
        #[cfg(feature = "tracing")]
56
        tracing::debug!("Using file backend (sandboxed mode)");
57

            
58
        let secret = Secret::sandboxed().await?;
59
        let path = crate::file::api::Keyring::default_path()?;
60
        Self::sandboxed_with_path(&path, secret).await
61
    }
62

            
63
    /// Use the file backend with a custom path.
64
    ///
65
    /// # Arguments
66
    /// * `path` - Path to the keyring file
67
    /// * `secret` - Secret to unlock the keyring (use `Secret::sandboxed()` or
68
    ///   `Secret::random()` for tests)
69
7
    pub async fn sandboxed_with_path(
70
        path: impl AsRef<std::path::Path>,
71
        secret: Secret,
72
    ) -> Result<Self> {
73
12
        #[cfg(feature = "tracing")]
74
        tracing::debug!("Using file backend with custom path");
75

            
76
16
        let file = file::UnlockedKeyring::load(path, secret.clone()).await?;
77
6
        Ok(Self::File(
78
10
            Arc::new(RwLock::new(Some(file::Keyring::Unlocked(file)))),
79
6
            secret,
80
        ))
81
    }
82

            
83
    /// Use the D-Bus Secret Service.
84
    pub async fn host() -> Result<Self> {
85
        #[cfg(feature = "tracing")]
86
        tracing::debug!("Using D-Bus Secret Service (host mode)");
87

            
88
        let service = dbus::Service::new().await?;
89
        let collection = service.default_collection().await?;
90
        Ok(Self::DBus(collection))
91
    }
92

            
93
    /// Use the D-Bus Secret Service with a custom connection.
94
19
    pub async fn host_with_connection(connection: zbus::Connection) -> Result<Self> {
95
8
        #[cfg(feature = "tracing")]
96
        tracing::debug!("Using D-Bus Secret Service with custom connection (test mode)");
97

            
98
10
        let service = dbus::Service::new_with_connection(&connection).await?;
99
13
        let collection = service.default_collection().await?;
100
3
        Ok(Self::DBus(collection))
101
    }
102

            
103
    /// Unlock the used collection.
104
8
    pub async fn unlock(&self) -> Result<()> {
105
2
        match self {
106
6
            Self::DBus(backend) => backend.unlock(None).await?,
107
2
            Self::File(keyring, secret) => {
108
4
                let mut kg = keyring.write().await;
109
4
                let kg_value = kg.take();
110
6
                if let Some(file::Keyring::Locked(locked)) = kg_value {
111
4
                    #[cfg(feature = "tracing")]
112
                    tracing::debug!("Unlocking file backend keyring");
113

            
114
12
                    let unlocked = locked
115
4
                        .unlock(secret.clone())
116
6
                        .await
117
2
                        .map_err(crate::Error::File)?;
118
2
                    *kg = Some(file::Keyring::Unlocked(unlocked));
119
                } else {
120
2
                    *kg = kg_value;
121
                }
122
            }
123
        };
124
2
        Ok(())
125
    }
126

            
127
    /// Lock the used collection.
128
8
    pub async fn lock(&self) -> Result<()> {
129
2
        match self {
130
6
            Self::DBus(backend) => backend.lock(None).await?,
131
2
            Self::File(keyring, _) => {
132
4
                let mut kg = keyring.write().await;
133
4
                let kg_value = kg.take();
134
6
                if let Some(file::Keyring::Unlocked(unlocked)) = kg_value {
135
4
                    #[cfg(feature = "tracing")]
136
                    tracing::debug!("Locking file backend keyring");
137

            
138
2
                    let locked = unlocked.lock();
139
2
                    *kg = Some(file::Keyring::Locked(locked));
140
                } else {
141
2
                    *kg = kg_value;
142
                }
143
            }
144
        };
145
2
        Ok(())
146
    }
147

            
148
    /// Whether the keyring is locked or not.
149
8
    pub async fn is_locked(&self) -> Result<bool> {
150
2
        match self {
151
6
            Self::DBus(collection) => collection.is_locked().await.map_err(From::from),
152
2
            Self::File(keyring, _) => {
153
4
                let keyring_guard = keyring.read().await;
154
4
                Ok(keyring_guard
155
2
                    .as_ref()
156
2
                    .expect("Keyring must exist")
157
2
                    .is_locked())
158
            }
159
        }
160
    }
161

            
162
    /// Remove items that matches the attributes.
163
38
    pub async fn delete(&self, attributes: &impl AsAttributes) -> Result<()> {
164
6
        match self {
165
6
            Self::DBus(backend) => {
166
18
                let items = backend.search_items(attributes).await?;
167
24
                for item in items {
168
30
                    item.delete(None).await?;
169
                }
170
            }
171
6
            Self::File(keyring, _) => {
172
12
                let kg = keyring.read().await;
173
12
                match kg.as_ref() {
174
6
                    Some(file::Keyring::Unlocked(backend)) => {
175
27
                        backend
176
6
                            .delete(attributes)
177
27
                            .await
178
7
                            .map_err(crate::Error::File)?;
179
                    }
180
                    Some(file::Keyring::Locked(_)) => {
181
2
                        return Err(crate::file::Error::Locked.into());
182
                    }
183
                    _ => unreachable!("A keyring must exist"),
184
                }
185
            }
186
        };
187
7
        Ok(())
188
    }
189

            
190
    /// Retrieve all the items.
191
8
    pub async fn items(&self) -> Result<Vec<Item>> {
192
2
        let items = match self {
193
2
            Self::DBus(backend) => {
194
6
                let items = backend.items().await?;
195
4
                items.into_iter().map(Item::for_dbus).collect::<Vec<_>>()
196
            }
197
2
            Self::File(keyring, _) => {
198
4
                let kg = keyring.read().await;
199
4
                match kg.as_ref() {
200
2
                    Some(file::Keyring::Unlocked(backend)) => {
201
4
                        let items = backend.items().await.map_err(crate::Error::File)?;
202
2
                        items
203
                            .into_iter()
204
6
                            .map(|i| Item::for_file(i.into(), Arc::clone(keyring)))
205
                            .collect::<Vec<_>>()
206
                    }
207
                    Some(file::Keyring::Locked(_)) => {
208
2
                        return Err(crate::file::Error::Locked.into());
209
                    }
210
                    _ => unreachable!("A keyring must exist"),
211
                }
212
            }
213
        };
214
2
        Ok(items)
215
    }
216

            
217
    /// Create a new item.
218
7
    pub async fn create_item(
219
        &self,
220
        label: &str,
221
        attributes: &impl AsAttributes,
222
        secret: impl Into<Secret>,
223
        replace: bool,
224
    ) -> Result<()> {
225
7
        match self {
226
7
            Self::DBus(backend) => {
227
20
                backend
228
7
                    .create_item(label, attributes, secret, replace, None)
229
32
                    .await?;
230
            }
231
7
            Self::File(keyring, _) => {
232
14
                let kg = keyring.read().await;
233
14
                match kg.as_ref() {
234
7
                    Some(file::Keyring::Unlocked(backend)) => {
235
25
                        backend
236
7
                            .create_item(label, attributes, secret, replace)
237
26
                            .await
238
12
                            .map_err(crate::Error::File)?;
239
                    }
240
                    Some(file::Keyring::Locked(_)) => {
241
2
                        return Err(crate::file::Error::Locked.into());
242
                    }
243
                    _ => unreachable!("A keyring must exist"),
244
                }
245
            }
246
        };
247
6
        Ok(())
248
    }
249

            
250
    /// Find items based on their attributes.
251
24
    pub async fn search_items(&self, attributes: &impl AsAttributes) -> Result<Vec<Item>> {
252
6
        let items = match self {
253
6
            Self::DBus(backend) => {
254
18
                let items = backend.search_items(attributes).await?;
255
12
                items.into_iter().map(Item::for_dbus).collect::<Vec<_>>()
256
            }
257
6
            Self::File(keyring, _) => {
258
12
                let kg = keyring.read().await;
259
12
                match kg.as_ref() {
260
6
                    Some(file::Keyring::Unlocked(backend)) => {
261
24
                        let items = backend
262
6
                            .search_items(attributes)
263
18
                            .await
264
6
                            .map_err(crate::Error::File)?;
265
6
                        items
266
                            .into_iter()
267
18
                            .map(|i| Item::for_file(i.into(), Arc::clone(keyring)))
268
                            .collect::<Vec<_>>()
269
                    }
270
                    Some(file::Keyring::Locked(_)) => {
271
2
                        return Err(crate::file::Error::Locked.into());
272
                    }
273
                    _ => unreachable!("A keyring must exist"),
274
                }
275
            }
276
        };
277
6
        Ok(items)
278
    }
279
}
280

            
281
/// A generic secret with a label and attributes.
282
#[derive(Debug)]
283
pub enum Item {
284
    #[doc(hidden)]
285
    File(
286
        RwLock<Option<file::Item>>,
287
        Arc<RwLock<Option<file::Keyring>>>,
288
    ),
289
    #[doc(hidden)]
290
    DBus(dbus::Item),
291
}
292

            
293
impl Item {
294
2
    fn for_file(item: file::Item, backend: Arc<RwLock<Option<file::Keyring>>>) -> Self {
295
4
        Self::File(RwLock::new(Some(item)), backend)
296
    }
297

            
298
2
    fn for_dbus(item: dbus::Item) -> Self {
299
2
        Self::DBus(item)
300
    }
301

            
302
    /// The item label.
303
10
    pub async fn label(&self) -> Result<String> {
304
2
        let label = match self {
305
2
            Self::File(item, _) => {
306
4
                let item_guard = item.read().await;
307
4
                let file_item = item_guard.as_ref().expect("Item must exist");
308
2
                match file_item {
309
4
                    file::Item::Unlocked(unlocked) => unlocked.label().to_owned(),
310
2
                    file::Item::Locked(_) => return Err(crate::file::Error::Locked.into()),
311
                }
312
            }
313
6
            Self::DBus(item) => item.label().await?,
314
        };
315
2
        Ok(label)
316
    }
317

            
318
    /// Sets the item label.
319
12
    pub async fn set_label(&self, label: &str) -> Result<()> {
320
2
        match self {
321
2
            Self::File(item, keyring) => {
322
4
                let mut item_guard = item.write().await;
323
4
                let file_item = item_guard.as_mut().expect("Item must exist");
324

            
325
2
                match file_item {
326
2
                    file::Item::Unlocked(unlocked) => {
327
2
                        unlocked.set_label(label);
328

            
329
2
                        let kg = keyring.read().await;
330
4
                        match kg.as_ref() {
331
2
                            Some(file::Keyring::Unlocked(backend)) => {
332
8
                                backend
333
                                    .create_item(
334
2
                                        unlocked.label(),
335
2
                                        &unlocked.attributes(),
336
2
                                        unlocked.secret(),
337
                                        true,
338
                                    )
339
8
                                    .await
340
4
                                    .map_err(crate::Error::File)?;
341
                            }
342
                            Some(file::Keyring::Locked(_)) => {
343
2
                                return Err(crate::file::Error::Locked.into());
344
                            }
345
                            None => unreachable!("A keyring must exist"),
346
                        }
347
                    }
348
                    file::Item::Locked(_) => {
349
2
                        return Err(crate::file::Error::Locked.into());
350
                    }
351
                }
352
            }
353
6
            Self::DBus(item) => item.set_label(label).await?,
354
        };
355
2
        Ok(())
356
    }
357

            
358
    /// Retrieve the item attributes.
359
10
    pub async fn attributes(&self) -> Result<HashMap<String, String>> {
360
2
        let attributes = match self {
361
2
            Self::File(item, _) => {
362
4
                let item_guard = item.read().await;
363
4
                let file_item = item_guard.as_ref().expect("Item must exist");
364
2
                match file_item {
365
2
                    file::Item::Unlocked(unlocked) => unlocked
366
                        .attributes()
367
                        .iter()
368
6
                        .map(|(k, v)| (k.to_owned(), v.to_string()))
369
                        .collect::<HashMap<_, _>>(),
370
2
                    file::Item::Locked(_) => return Err(crate::file::Error::Locked.into()),
371
                }
372
            }
373
6
            Self::DBus(item) => item.attributes().await?,
374
        };
375
2
        Ok(attributes)
376
    }
377

            
378
    /// Retrieve the item attributes as a typed schema.
379
    ///
380
    /// # Example
381
    ///
382
    /// ```no_run
383
    /// # use oo7::{SecretSchema, Item};
384
    /// # #[derive(SecretSchema, Debug)]
385
    /// # #[schema(name = "org.example.Password")]
386
    /// # struct PasswordSchema {
387
    /// #     username: String,
388
    /// #     server: String,
389
    /// # }
390
    /// # async fn example(item: &Item) -> Result<(), oo7::Error> {
391
    /// let schema = item.attributes_as::<PasswordSchema>().await?;
392
    /// println!("Username: {}", schema.username);
393
    /// # Ok(())
394
    /// # }
395
    /// ```
396
    #[cfg(feature = "schema")]
397
    #[cfg_attr(docsrs, doc(cfg(feature = "schema")))]
398
4
    pub async fn attributes_as<T>(&self) -> Result<T>
399
    where
400
        T: for<'a> std::convert::TryFrom<&'a HashMap<String, String>, Error = crate::SchemaError>,
401
    {
402
2
        match self {
403
4
            Self::File(..) => T::try_from(&self.attributes().await?)
404
2
                .map_err(crate::file::Error::Schema)
405
2
                .map_err(Into::into),
406
4
            Self::DBus(_) => T::try_from(&self.attributes().await?)
407
2
                .map_err(crate::dbus::Error::Schema)
408
2
                .map_err(Into::into),
409
        }
410
    }
411

            
412
    /// Sets the item attributes.
413
24
    pub async fn set_attributes(&self, attributes: &impl AsAttributes) -> Result<()> {
414
4
        match self {
415
4
            Self::File(item, keyring) => {
416
8
                let kg = keyring.read().await;
417

            
418
8
                match kg.as_ref() {
419
4
                    Some(file::Keyring::Unlocked(backend)) => {
420
8
                        let mut item_guard = item.write().await;
421
8
                        let file_item = item_guard.as_mut().expect("Item must exist");
422

            
423
4
                        match file_item {
424
2
                            file::Item::Unlocked(unlocked) => {
425
10
                                let index = backend
426
4
                                    .lookup_item_index(&unlocked.attributes())
427
6
                                    .await
428
2
                                    .map_err(crate::Error::File)?;
429

            
430
2
                                unlocked.set_attributes(attributes);
431

            
432
4
                                if let Some(index) = index {
433
10
                                    backend
434
2
                                        .replace_item_index(index, unlocked)
435
8
                                        .await
436
2
                                        .map_err(crate::Error::File)?;
437
                                } else {
438
10
                                    backend
439
                                        .create_item(
440
2
                                            unlocked.label(),
441
2
                                            attributes,
442
2
                                            unlocked.secret(),
443
                                            true,
444
                                        )
445
8
                                        .await
446
4
                                        .map_err(crate::Error::File)?;
447
                                }
448
                            }
449
                            file::Item::Locked(_) => {
450
2
                                return Err(crate::file::Error::Locked.into());
451
                            }
452
                        }
453
                    }
454
                    Some(file::Keyring::Locked(_)) => {
455
2
                        return Err(crate::file::Error::Locked.into());
456
                    }
457
                    None => unreachable!("A keyring must exist"),
458
                }
459
            }
460
12
            Self::DBus(item) => item.set_attributes(attributes).await?,
461
        };
462
2
        Ok(())
463
    }
464

            
465
    /// Sets a new secret.
466
12
    pub async fn set_secret(&self, secret: impl Into<Secret>) -> Result<()> {
467
2
        match self {
468
2
            Self::File(item, keyring) => {
469
4
                let mut item_guard = item.write().await;
470
4
                let file_item = item_guard.as_mut().expect("Item must exist");
471

            
472
2
                match file_item {
473
2
                    file::Item::Unlocked(unlocked) => {
474
2
                        unlocked.set_secret(secret);
475

            
476
2
                        let kg = keyring.read().await;
477
4
                        match kg.as_ref() {
478
2
                            Some(file::Keyring::Unlocked(backend)) => {
479
8
                                backend
480
                                    .create_item(
481
2
                                        unlocked.label(),
482
2
                                        &unlocked.attributes(),
483
2
                                        unlocked.secret(),
484
                                        true,
485
                                    )
486
8
                                    .await
487
4
                                    .map_err(crate::Error::File)?;
488
                            }
489
                            Some(file::Keyring::Locked(_)) => {
490
2
                                return Err(crate::file::Error::Locked.into());
491
                            }
492
                            None => unreachable!("A keyring must exist"),
493
                        }
494
                    }
495
                    file::Item::Locked(_) => {
496
2
                        return Err(crate::file::Error::Locked.into());
497
                    }
498
                }
499
            }
500
8
            Self::DBus(item) => item.set_secret(secret).await?,
501
        };
502
2
        Ok(())
503
    }
504

            
505
    /// Retrieves the stored secret.
506
10
    pub async fn secret(&self) -> Result<Secret> {
507
2
        let secret = match self {
508
2
            Self::File(item, _) => {
509
4
                let item_guard = item.read().await;
510
4
                let file_item = item_guard.as_ref().expect("Item must exist");
511
2
                match file_item {
512
2
                    file::Item::Unlocked(unlocked) => unlocked.secret(),
513
2
                    file::Item::Locked(_) => return Err(crate::file::Error::Locked.into()),
514
                }
515
            }
516
6
            Self::DBus(item) => item.secret().await?,
517
        };
518
2
        Ok(secret)
519
    }
520

            
521
    /// Whether the item is locked or not
522
14
    pub async fn is_locked(&self) -> Result<bool> {
523
4
        match self {
524
6
            Self::DBus(item) => item.is_locked().await.map_err(From::from),
525
4
            Self::File(item, _) => {
526
6
                let item_guard = item.read().await;
527
6
                let file_item = item_guard.as_ref().expect("Item must exist");
528
4
                Ok(file_item.is_locked())
529
            }
530
        }
531
    }
532

            
533
    /// Lock the item
534
8
    pub async fn lock(&self) -> Result<()> {
535
2
        match self {
536
8
            Self::DBus(item) => item.lock(None).await?,
537
2
            Self::File(item, keyring) => {
538
4
                let mut item_guard = item.write().await;
539
4
                let item_value = item_guard.take();
540
6
                if let Some(file::Item::Unlocked(unlocked)) = item_value {
541
4
                    let kg = keyring.read().await;
542
4
                    match kg.as_ref() {
543
2
                        Some(file::Keyring::Unlocked(backend)) => {
544
8
                            let locked = backend
545
2
                                .lock_item(unlocked)
546
6
                                .await
547
2
                                .map_err(crate::Error::File)?;
548
2
                            *item_guard = Some(file::Item::Locked(locked));
549
                        }
550
                        Some(file::Keyring::Locked(_)) => {
551
2
                            *item_guard = Some(file::Item::Unlocked(unlocked));
552
2
                            return Err(crate::file::Error::Locked.into());
553
                        }
554
                        None => unreachable!("A keyring must exist"),
555
                    }
556
                } else {
557
2
                    *item_guard = item_value;
558
                }
559
            }
560
        }
561
2
        Ok(())
562
    }
563

            
564
    /// Unlock the item
565
8
    pub async fn unlock(&self) -> Result<()> {
566
2
        match self {
567
6
            Self::DBus(item) => item.unlock(None).await?,
568
2
            Self::File(item, keyring) => {
569
4
                let mut item_guard = item.write().await;
570
4
                let item_value = item_guard.take();
571
6
                if let Some(file::Item::Locked(locked)) = item_value {
572
4
                    let kg = keyring.read().await;
573
4
                    match kg.as_ref() {
574
2
                        Some(file::Keyring::Unlocked(backend)) => {
575
8
                            let unlocked = backend
576
2
                                .unlock_item(locked)
577
6
                                .await
578
2
                                .map_err(crate::Error::File)?;
579
2
                            *item_guard = Some(file::Item::Unlocked(unlocked));
580
                        }
581
                        Some(file::Keyring::Locked(_)) => {
582
                            *item_guard = Some(file::Item::Locked(locked));
583
                            return Err(crate::file::Error::Locked.into());
584
                        }
585
                        None => unreachable!("A keyring must exist"),
586
                    }
587
                } else {
588
2
                    *item_guard = item_value;
589
                }
590
            }
591
        }
592
2
        Ok(())
593
    }
594

            
595
    /// Delete the item.
596
12
    pub async fn delete(&self) -> Result<()> {
597
2
        match self {
598
2
            Self::File(item, keyring) => {
599
4
                let item_guard = item.read().await;
600
4
                let file_item = item_guard.as_ref().expect("Item must exist");
601

            
602
2
                match file_item {
603
2
                    file::Item::Unlocked(unlocked) => {
604
4
                        let kg = keyring.read().await;
605
4
                        match kg.as_ref() {
606
2
                            Some(file::Keyring::Unlocked(backend)) => {
607
8
                                backend
608
4
                                    .delete(&unlocked.attributes())
609
8
                                    .await
610
2
                                    .map_err(crate::Error::File)?;
611
                            }
612
                            Some(file::Keyring::Locked(_)) => {
613
2
                                return Err(crate::file::Error::Locked.into());
614
                            }
615
                            None => unreachable!("A keyring must exist"),
616
                        }
617
                    }
618
                    file::Item::Locked(_) => {
619
                        return Err(crate::file::Error::Locked.into());
620
                    }
621
                }
622
            }
623
2
            Self::DBus(item) => {
624
6
                item.delete(None).await?;
625
            }
626
        };
627
2
        Ok(())
628
    }
629

            
630
    /// The UNIX time when the item was created.
631
8
    pub async fn created(&self) -> Result<Duration> {
632
2
        match self {
633
8
            Self::DBus(item) => Ok(item.created().await?),
634
2
            Self::File(item, _) => {
635
4
                let item_guard = item.read().await;
636
4
                let file_item = item_guard.as_ref().expect("Item must exist");
637
2
                match file_item {
638
4
                    file::Item::Unlocked(unlocked) => Ok(unlocked.created()),
639
2
                    file::Item::Locked(_) => Err(crate::file::Error::Locked.into()),
640
                }
641
            }
642
        }
643
    }
644

            
645
    /// The UNIX time when the item was modified.
646
8
    pub async fn modified(&self) -> Result<Duration> {
647
2
        match self {
648
8
            Self::DBus(item) => Ok(item.modified().await?),
649
2
            Self::File(item, _) => {
650
4
                let item_guard = item.read().await;
651
4
                let file_item = item_guard.as_ref().expect("Item must exist");
652
2
                match file_item {
653
4
                    file::Item::Unlocked(unlocked) => Ok(unlocked.modified()),
654
2
                    file::Item::Locked(_) => Err(crate::file::Error::Locked.into()),
655
                }
656
            }
657
        }
658
    }
659
}