1
// org.freedesktop.Secret.Service
2

            
3
use std::{
4
    collections::HashMap,
5
    sync::{Arc, OnceLock},
6
};
7

            
8
use oo7::{
9
    Key, Secret,
10
    dbus::{
11
        Algorithm, ServiceError,
12
        api::{DBusSecretInner, Properties},
13
    },
14
    file::{Keyring, LockedKeyring, UnlockedKeyring},
15
};
16
use tokio::sync::{Mutex, RwLock};
17
use tokio_stream::StreamExt;
18
use zbus::{
19
    names::UniqueName,
20
    object_server::SignalEmitter,
21
    proxy::Defaults,
22
    zvariant::{ObjectPath, Optional, OwnedObjectPath, OwnedValue, Value},
23
};
24

            
25
#[cfg(any(feature = "gnome_native_crypto", feature = "gnome_openssl_crypto"))]
26
pub use crate::gnome::internal::{INTERNAL_INTERFACE_PATH, InternalInterface};
27
#[cfg(any(feature = "plasma_native_crypto", feature = "plasma_openssl_crypto"))]
28
use crate::plasma::prompter::in_plasma_environment;
29
use crate::{
30
    collection::Collection,
31
    error::{Error, custom_service_error},
32
    migration::PendingMigration,
33
    prompt::{Prompt, PromptAction, PromptRole},
34
    session::Session,
35
};
36

            
37
const DEFAULT_COLLECTION_ALIAS_PATH: ObjectPath<'static> =
38
    ObjectPath::from_static_str_unchecked("/org/freedesktop/secrets/aliases/default");
39

            
40
/// Prompter type
41
#[derive(Clone, Copy, PartialEq, Eq)]
42
pub enum PrompterType {
43
    #[allow(clippy::upper_case_acronyms)]
44
    GNOME,
45
    Plasma,
46
}
47

            
48
#[derive(Clone)]
49
pub struct Service {
50
    // Properties
51
    pub(crate) collections: Arc<Mutex<HashMap<OwnedObjectPath, Collection>>>,
52
    // Other attributes
53
    connection: Arc<OnceLock<zbus::Connection>>,
54
    // sessions mapped to their corresponding object path on the bus
55
    sessions: Arc<Mutex<HashMap<OwnedObjectPath, Session>>>,
56
    session_index: Arc<RwLock<u32>>,
57
    // prompts mapped to their corresponding object path on the bus
58
    prompts: Arc<Mutex<HashMap<OwnedObjectPath, Prompt>>>,
59
    prompt_index: Arc<RwLock<u32>>,
60
    // pending collection creations: prompt_path -> (label, alias)
61
    pending_collections: Arc<Mutex<HashMap<OwnedObjectPath, (String, String)>>>,
62
    // pending keyring migrations: name -> migration
63
    pub(crate) pending_migrations: Arc<Mutex<HashMap<String, PendingMigration>>>,
64
    // Data directory for keyrings (e.g., ~/.local/share or test temp dir)
65
    data_dir: std::path::PathBuf,
66
    // PAM socket path (None for tests that don't need PAM listener)
67
    pub(crate) pam_socket: Option<std::path::PathBuf>,
68
    // Override for prompter type (mainly for tests)
69
    pub(crate) prompter_type_override: Arc<Mutex<Option<PrompterType>>>,
70
}
71

            
72
#[zbus::interface(name = "org.freedesktop.Secret.Service")]
73
impl Service {
74
    #[zbus(out_args("output", "result"))]
75
32
    pub async fn open_session(
76
        &self,
77
        algorithm: Algorithm,
78
        input: Value<'_>,
79
        #[zbus(header)] header: zbus::message::Header<'_>,
80
        #[zbus(object_server)] object_server: &zbus::ObjectServer,
81
    ) -> Result<(OwnedValue, OwnedObjectPath), ServiceError> {
82
63
        let (public_key, aes_key) = match algorithm {
83
27
            Algorithm::Plain => (None, None),
84
            Algorithm::Encrypted => {
85
30
                let client_public_key = Key::try_from(input).map_err(|err| {
86
                    custom_service_error(&format!(
87
                        "Input Value could not be converted into a Key {err}."
88
                    ))
89
                })?;
90
32
                let private_key = Key::generate_private_key().map_err(|err| {
91
                    custom_service_error(&format!("Failed to generate private key {err}."))
92
                })?;
93
                (
94
34
                    Some(Key::generate_public_key(&private_key).map_err(|err| {
95
                        custom_service_error(&format!("Failed to generate public key {err}."))
96
                    })?),
97
16
                    Some(
98
31
                        Key::generate_aes_key(&private_key, &client_public_key).map_err(|err| {
99
                            custom_service_error(&format!("Failed to generate aes key {err}."))
100
                        })?,
101
                    ),
102
                )
103
            }
104
        };
105

            
106
83
        let sender = if let Some(s) = header.sender() {
107
            s.to_owned()
108
        } else {
109
            #[cfg(any(test, feature = "test-util"))]
110
            {
111
                // For p2p test connections, use a dummy sender since p2p connections
112
                // don't have a bus to assign unique names
113
59
                UniqueName::try_from(":p2p.test").unwrap()
114
            }
115
            #[cfg(not(any(test, feature = "test-util")))]
116
            {
117
                return Err(custom_service_error("Failed to get sender from header."));
118
            }
119
        };
120

            
121
59
        tracing::info!("Client {} connected", sender);
122

            
123
61
        let session = Session::new(aes_key.map(Arc::new), self.clone(), sender).await;
124
58
        let path = OwnedObjectPath::from(session.path().clone());
125

            
126
147
        self.sessions
127
            .lock()
128
85
            .await
129
55
            .insert(path.clone(), session.clone());
130

            
131
30
        object_server.at(&path, session).await?;
132

            
133
28
        let service_key = public_key
134
29
            .map(OwnedValue::from)
135
81
            .unwrap_or_else(|| Value::new::<Vec<u8>>(vec![]).try_into_owned().unwrap());
136

            
137
30
        Ok((service_key, path))
138
    }
139

            
140
    #[zbus(out_args("collection", "prompt"))]
141
10
    pub async fn create_collection(
142
        &self,
143
        properties: Properties,
144
        alias: &str,
145
    ) -> Result<(OwnedObjectPath, ObjectPath<'_>), ServiceError> {
146
19
        let label = properties.label().to_owned();
147
21
        let alias = alias.to_owned();
148

            
149
        // Create a prompt to get the password for the new collection
150
        let prompt = Prompt::new(
151
21
            self.clone(),
152
            PromptRole::CreateCollection,
153
10
            label.clone(),
154
10
            None,
155
        )
156
26
        .await;
157
18
        let prompt_path = OwnedObjectPath::from(prompt.path().clone());
158

            
159
        // Store the collection metadata for later creation
160
40
        self.pending_collections
161
            .lock()
162
24
            .await
163
8
            .insert(prompt_path.clone(), (label, alias));
164

            
165
        // Create the collection creation action
166
18
        let service = self.clone();
167
10
        let creation_prompt_path = prompt_path.clone();
168
48
        let action = PromptAction::new(move |secret: Secret| async move {
169
46
            let collection_path = service
170
20
                .complete_collection_creation(&creation_prompt_path, secret)
171
42
                .await?;
172

            
173
24
            Ok(Value::new(collection_path).try_into_owned().unwrap())
174
        });
175

            
176
10
        prompt.set_action(action).await;
177

            
178
        // Register the prompt
179
50
        self.prompts
180
            .lock()
181
24
            .await
182
16
            .insert(prompt_path.clone(), prompt.clone());
183

            
184
10
        self.object_server().at(&prompt_path, prompt).await?;
185

            
186
8
        tracing::debug!("CreateCollection prompt created at `{}`", prompt_path);
187

            
188
        // Return empty collection path and the prompt path
189
18
        Ok((OwnedObjectPath::default(), prompt_path.into()))
190
    }
191

            
192
    #[zbus(out_args("unlocked", "locked"))]
193
4
    pub async fn search_items(
194
        &self,
195
        attributes: HashMap<String, String>,
196
    ) -> Result<(Vec<OwnedObjectPath>, Vec<OwnedObjectPath>), ServiceError> {
197
4
        let mut unlocked = Vec::new();
198
4
        let mut locked = Vec::new();
199
8
        let collections = self.collections.lock().await;
200

            
201
12
        for (_path, collection) in collections.iter() {
202
16
            let items = collection.search_inner_items(&attributes).await?;
203
8
            for item in items {
204
16
                if item.is_locked().await {
205
8
                    locked.push(item.path().clone().into());
206
                } else {
207
9
                    unlocked.push(item.path().clone().into());
208
                }
209
            }
210
        }
211

            
212
8
        if unlocked.is_empty() && locked.is_empty() {
213
8
            tracing::debug!(
214
                "Items with attributes {:?} does not exist in any collection.",
215
                attributes
216
            );
217
        } else {
218
8
            tracing::debug!("Items with attributes {:?} found.", attributes);
219
        }
220

            
221
4
        Ok((unlocked, locked))
222
    }
223

            
224
    #[zbus(out_args("unlocked", "prompt"))]
225
10
    pub async fn unlock(
226
        &self,
227
        objects: Vec<OwnedObjectPath>,
228
    ) -> Result<(Vec<OwnedObjectPath>, OwnedObjectPath), ServiceError> {
229
20
        let (unlocked, not_unlocked) = self.set_locked(false, &objects).await?;
230
22
        if !not_unlocked.is_empty() {
231
            // Extract the label and collection before creating the prompt
232
18
            let label = self.extract_label_from_objects(&not_unlocked).await;
233
16
            let collection = self.extract_collection_from_objects(&not_unlocked).await;
234

            
235
16
            let prompt = Prompt::new(self.clone(), PromptRole::Unlock, label, collection).await;
236
16
            let path = OwnedObjectPath::from(prompt.path().clone());
237

            
238
            // Create the unlock action
239
8
            let service = self.clone();
240
56
            let action = PromptAction::new(move |secret: Secret| async move {
241
                // The prompter will handle secret validation
242
                // Here we just perform the unlock operation
243

            
244
                // First, check for pending migrations (without holding collections lock)
245
32
                for object in &not_unlocked {
246
                    let collection = {
247
24
                        let collections = service.collections.lock().await;
248
16
                        collections.get(object).cloned()
249
                    };
250

            
251
8
                    if let Some(collection) = collection {
252
                        // Check if this collection has a pending migration by name
253
                        let migration_opt = {
254
24
                            let pending = service.pending_migrations.lock().await;
255
16
                            pending.get(collection.name()).cloned()
256
                        };
257

            
258
8
                        if let Some(migration) = migration_opt {
259
8
                            let migration_name = migration.name();
260
5
                            tracing::debug!(
261
                                "Attempting migration for '{}' during unlock",
262
                                migration_name
263
                            );
264

            
265
                            // Attempt migration with the provided secret (no locks held)
266
16
                            match migration.migrate(&service.data_dir, &secret).await {
267
4
                                Ok(unlocked_keyring) => {
268
8
                                    tracing::info!(
269
                                        "Successfully migrated '{}' during unlock",
270
                                        migration_name
271
                                    );
272

            
273
                                    // Replace the keyring in the collection
274
12
                                    let mut keyring_guard = collection.keyring.write().await;
275
4
                                    *keyring_guard = Some(Keyring::Unlocked(unlocked_keyring));
276
4
                                    drop(keyring_guard);
277

            
278
                                    // Dispatch items from the migrated keyring
279
12
                                    if let Err(e) = collection.dispatch_items().await {
280
                                        tracing::error!(
281
                                            "Failed to dispatch items after migration: {}",
282
                                            e
283
                                        );
284
                                    }
285

            
286
                                    // Remove from pending migrations
287
16
                                    service
288
                                        .pending_migrations
289
4
                                        .lock()
290
16
                                        .await
291
4
                                        .remove(migration_name);
292
                                }
293
                                Err(e) => {
294
                                    tracing::warn!(
295
                                        "Failed to migrate '{}' during unlock: {}",
296
                                        migration_name,
297
                                        e
298
                                    );
299
                                    // Leave in pending_migrations, try normal unlock
300
                                    let _ =
301
                                        collection.set_locked(false, Some(secret.clone())).await;
302
                                }
303
                            }
304
                        } else {
305
                            // Normal unlock
306
24
                            let _ = collection.set_locked(false, Some(secret.clone())).await;
307
                        }
308
                    } else {
309
                        // Try to find as item within collections
310
12
                        let collections = service.collections.lock().await;
311
4
                        let mut found_collection = None;
312
8
                        for (_path, collection) in collections.iter() {
313
12
                            if let Some(item) = collection.item_from_path(object).await {
314
5
                                found_collection = Some((
315
4
                                    collection.clone(),
316
4
                                    item.clone(),
317
13
                                    collection.is_locked().await,
318
                                ));
319
                                break;
320
                            }
321
                        }
322
4
                        drop(collections);
323

            
324
5
                        if let Some((collection, item, is_locked)) = found_collection {
325
4
                            if is_locked {
326
14
                                let _ = collection.set_locked(false, Some(secret.clone())).await;
327
                            } else {
328
                                // Collection is already unlocked, just unlock the item
329
                                let keyring = collection.keyring.read().await;
330
                                let _ = item
331
                                    .set_locked(false, keyring.as_ref().unwrap().as_unlocked())
332
                                    .await;
333
                            }
334
                        }
335
                    }
336
                }
337
8
                Ok(Value::new(not_unlocked).try_into_owned().unwrap())
338
            });
339

            
340
8
            prompt.set_action(action).await;
341

            
342
41
            self.prompts
343
                .lock()
344
24
                .await
345
18
                .insert(path.clone(), prompt.clone());
346

            
347
9
            self.object_server().at(&path, prompt).await?;
348
8
            return Ok((unlocked, path));
349
        }
350

            
351
8
        Ok((unlocked, OwnedObjectPath::default()))
352
    }
353

            
354
    #[zbus(out_args("locked", "Prompt"))]
355
10
    pub async fn lock(
356
        &self,
357
        objects: Vec<OwnedObjectPath>,
358
    ) -> Result<(Vec<OwnedObjectPath>, OwnedObjectPath), ServiceError> {
359
        // set_locked now handles locking directly (without prompts)
360
22
        let (locked, not_locked) = self.set_locked(true, &objects).await?;
361
        // Locking never requires prompts, so not_locked should always be empty
362
        debug_assert!(
363
            not_locked.is_empty(),
364
            "Lock operation should never require prompts"
365
        );
366
10
        Ok((locked, OwnedObjectPath::default()))
367
    }
368

            
369
    #[zbus(out_args("secrets"))]
370
4
    pub async fn get_secrets(
371
        &self,
372
        items: Vec<OwnedObjectPath>,
373
        session: OwnedObjectPath,
374
    ) -> Result<HashMap<OwnedObjectPath, DBusSecretInner>, ServiceError> {
375
4
        let mut secrets = HashMap::new();
376
8
        let collections = self.collections.lock().await;
377

            
378
16
        'outer: for (_path, collection) in collections.iter() {
379
16
            for item in &items {
380
12
                if let Some(item) = collection.item_from_path(item).await {
381
12
                    match item.get_secret(session.clone()).await {
382
4
                        Ok((secret,)) => {
383
8
                            secrets.insert(item.path().clone().into(), secret);
384
                            // To avoid iterating through all the remaining collections, if the
385
                            // items secrets are already retrieved.
386
4
                            if secrets.len() == items.len() {
387
                                break 'outer;
388
                            }
389
                        }
390
                        // Avoid erroring out if an item is locked.
391
                        Err(ServiceError::IsLocked(_)) => {
392
                            continue;
393
                        }
394
4
                        Err(err) => {
395
4
                            return Err(err);
396
                        }
397
                    };
398
                }
399
            }
400
        }
401

            
402
4
        Ok(secrets)
403
    }
404

            
405
    #[zbus(out_args("collection"))]
406
85
    pub async fn read_alias(&self, name: &str) -> Result<OwnedObjectPath, ServiceError> {
407
        // Map "login" alias to "default" for compatibility with gnome-keyring
408
65
        let alias_to_find = if name == Self::LOGIN_ALIAS {
409
            oo7::dbus::Service::DEFAULT_COLLECTION
410
        } else {
411
19
            name
412
        };
413

            
414
19
        let collections = self.collections.lock().await;
415

            
416
87
        for (path, collection) in collections.iter() {
417
66
            if collection.alias().await == alias_to_find {
418
21
                tracing::debug!("Collection: {} found for alias: {}.", path, name);
419
41
                return Ok(path.to_owned());
420
            }
421
        }
422

            
423
6
        tracing::info!("Collection with alias {} does not exist.", name);
424

            
425
12
        Ok(OwnedObjectPath::default())
426
    }
427

            
428
4
    pub async fn set_alias(
429
        &self,
430
        name: &str,
431
        collection: OwnedObjectPath,
432
    ) -> Result<(), ServiceError> {
433
8
        let collections = self.collections.lock().await;
434

            
435
12
        for (path, other_collection) in collections.iter() {
436
8
            if *path == collection {
437
4
                other_collection.set_alias(name).await;
438

            
439
4
                tracing::info!("Collection: {} alias updated to {}.", collection, name);
440
4
                return Ok(());
441
            }
442
        }
443

            
444
4
        tracing::info!("Collection: {} does not exist.", collection);
445

            
446
8
        Err(ServiceError::NoSuchObject(format!(
447
            "The collection: {collection} does not exist.",
448
        )))
449
    }
450

            
451
    #[zbus(property, name = "Collections")]
452
109
    pub async fn collections(&self) -> Vec<OwnedObjectPath> {
453
64
        self.collections.lock().await.keys().cloned().collect()
454
    }
455

            
456
    #[zbus(signal, name = "CollectionCreated")]
457
    pub async fn collection_created(
458
11
        signal_emitter: &SignalEmitter<'_>,
459
9
        collection: &ObjectPath<'_>,
460
    ) -> zbus::Result<()>;
461

            
462
    #[zbus(signal, name = "CollectionDeleted")]
463
    pub async fn collection_deleted(
464
8
        signal_emitter: &SignalEmitter<'_>,
465
8
        collection: &ObjectPath<'_>,
466
    ) -> zbus::Result<()>;
467

            
468
    #[zbus(signal, name = "CollectionChanged")]
469
    pub async fn collection_changed(
470
10
        signal_emitter: &SignalEmitter<'_>,
471
10
        collection: &ObjectPath<'_>,
472
    ) -> zbus::Result<()>;
473
}
474

            
475
impl Service {
476
    const LOGIN_ALIAS: &str = "login";
477

            
478
    /// Set the prompter type override
479
    #[cfg(test)]
480
16
    pub(crate) async fn set_prompter_type(&self, prompter_type: PrompterType) {
481
8
        *self.prompter_type_override.lock().await = Some(prompter_type);
482
    }
483

            
484
    /// Get the prompter type to use
485
44
    pub(crate) async fn prompter_type(&self) -> PrompterType {
486
34
        if let Some(override_type) = self.prompter_type_override.lock().await.as_ref() {
487
4
            return *override_type;
488
        }
489

            
490
        #[cfg(any(feature = "plasma_native_crypto", feature = "plasma_openssl_crypto"))]
491
        {
492
10
            if in_plasma_environment(self.connection()).await {
493
                return PrompterType::Plasma;
494
            }
495
        }
496

            
497
14
        PrompterType::GNOME
498
    }
499

            
500
32
    pub(crate) fn new(
501
        data_dir: std::path::PathBuf,
502
        pam_socket: Option<std::path::PathBuf>,
503
    ) -> Self {
504
        Self {
505
64
            collections: Arc::new(Mutex::new(HashMap::new())),
506
64
            connection: Arc::new(OnceLock::new()),
507
64
            sessions: Arc::new(Mutex::new(HashMap::new())),
508
62
            session_index: Arc::new(RwLock::new(0)),
509
57
            prompts: Arc::new(Mutex::new(HashMap::new())),
510
60
            prompt_index: Arc::new(RwLock::new(0)),
511
60
            pending_collections: Arc::new(Mutex::new(HashMap::new())),
512
60
            pending_migrations: Arc::new(Mutex::new(HashMap::new())),
513
            data_dir,
514
            pam_socket,
515
59
            prompter_type_override: Arc::new(Mutex::new(None)),
516
        }
517
    }
518

            
519
    pub async fn run(secret: Option<Secret>, request_replacement: bool) -> Result<(), Error> {
520
        // Compute data directory from environment variables
521
        let data_dir = std::env::var_os("XDG_DATA_HOME")
522
            .and_then(|h| if h.is_empty() { None } else { Some(h) })
523
            .map(std::path::PathBuf::from)
524
            .and_then(|p| if p.is_absolute() { Some(p) } else { None })
525
            .or_else(|| {
526
                std::env::var_os("HOME")
527
                    .and_then(|h| if h.is_empty() { None } else { Some(h) })
528
                    .map(std::path::PathBuf::from)
529
                    .map(|p| p.join(".local/share"))
530
            })
531
            .ok_or_else(|| {
532
                Error::IO(std::io::Error::new(
533
                    std::io::ErrorKind::NotFound,
534
                    "No data directory found (XDG_DATA_HOME or HOME)",
535
                ))
536
            })?;
537

            
538
        // Compute PAM socket path from environment variable
539
        let pam_socket = std::env::var_os("OO7_PAM_SOCKET").map(std::path::PathBuf::from);
540

            
541
        let service = Self::new(data_dir, pam_socket);
542

            
543
        let connection = zbus::connection::Builder::session()?
544
            .allow_name_replacements(true)
545
            .replace_existing_names(request_replacement)
546
            .name(oo7::dbus::api::Service::DESTINATION.as_deref().unwrap())?
547
            .serve_at(
548
                oo7::dbus::api::Service::PATH.as_deref().unwrap(),
549
                service.clone(),
550
            )?
551
            .build()
552
            .await?;
553

            
554
        #[cfg(any(feature = "gnome_native_crypto", feature = "gnome_openssl_crypto"))]
555
        connection
556
            .object_server()
557
            .at(
558
                INTERNAL_INTERFACE_PATH,
559
                InternalInterface::new(service.clone()),
560
            )
561
            .await?;
562

            
563
        // Discover existing keyrings
564
        let discovered_keyrings = service.discover_keyrings(secret.clone()).await?;
565

            
566
        service
567
            .initialize(connection, discovered_keyrings, secret, true)
568
            .await?;
569

            
570
        // Start PAM listener
571
        tracing::info!("Starting PAM listener");
572
        let pam_listener = crate::pam_listener::PamListener::new(service.clone());
573
        tokio::spawn(async move {
574
            if let Err(e) = pam_listener.start().await {
575
                tracing::error!("PAM listener error: {}", e);
576
            }
577
        });
578

            
579
        Ok(())
580
    }
581

            
582
    #[cfg(any(test, feature = "test-util"))]
583
    #[allow(dead_code)]
584
33
    pub async fn run_with_connection(
585
        connection: zbus::Connection,
586
        data_dir: std::path::PathBuf,
587
        pam_socket: Option<std::path::PathBuf>,
588
        secret: Option<Secret>,
589
    ) -> Result<Self, Error> {
590
32
        let service = Self::new(data_dir, pam_socket);
591

            
592
        // Serve the service at the standard path
593
133
        connection
594
            .object_server()
595
            .at(
596
28
                oo7::dbus::api::Service::PATH.as_deref().unwrap(),
597
29
                service.clone(),
598
            )
599
64
            .await?;
600

            
601
        #[cfg(any(feature = "gnome_native_crypto", feature = "gnome_openssl_crypto"))]
602
124
        connection
603
            .object_server()
604
            .at(
605
                INTERNAL_INTERFACE_PATH,
606
36
                InternalInterface::new(service.clone()),
607
            )
608
107
            .await?;
609

            
610
36
        let default_keyring = if let Some(secret) = secret.clone() {
611
101
            vec![(
612
36
                "default".to_owned(),
613
34
                "Login".to_owned(),
614
34
                oo7::dbus::Service::DEFAULT_COLLECTION.to_owned(),
615
95
                Keyring::Unlocked(UnlockedKeyring::temporary(secret).await?),
616
            )]
617
        } else {
618
12
            vec![]
619
        };
620

            
621
120
        service
622
27
            .initialize(connection, default_keyring, secret, false)
623
117
            .await?;
624
37
        Ok(service)
625
    }
626

            
627
    /// Generate a unique label and alias by checking registered
628
    /// collections and appending a counter if needed. Returns a tuple of
629
    /// (label, alias).
630
35
    fn make_unique_label_and_alias(
631
        collections: &HashMap<OwnedObjectPath, Collection>,
632
        label: &str,
633
        alias: &str,
634
    ) -> (String, String) {
635
        // Sanitize the label to create the path (for checking uniqueness)
636
24
        let base_path = crate::collection::collection_path(label)
637
            .expect("Sanitized label should always produce valid object path");
638
61
        if !collections.contains_key(&base_path) {
639
55
            return (label.to_owned(), alias.to_owned());
640
        }
641

            
642
        // Append counter until we find a unique one
643
4
        let mut counter = 2;
644
4
        loop {
645
8
            let path = crate::collection::collection_path(&format!("{label}{counter}"))
646
                .expect("Sanitized label should always produce valid object path");
647
4
            let new_label = format!("{}{}", label, counter);
648
8
            let new_alias = format!("{}{}", alias, counter);
649

            
650
8
            if !collections.contains_key(&path) {
651
4
                return (new_label, new_alias);
652
            }
653
            counter += 1;
654
        }
655
    }
656

            
657
    /// Discover existing keyrings in the data directory
658
    /// Returns a vector of (name, label, alias, keyring) tuples
659
4
    pub(crate) async fn discover_keyrings(
660
        &self,
661
        secret: Option<Secret>,
662
    ) -> Result<Vec<(String, String, String, Keyring)>, Error> {
663
4
        let mut discovered = Vec::new();
664

            
665
8
        let keyrings_dir = self.data_dir.join("keyrings");
666

            
667
        // Scan for v1 keyrings first
668
8
        let v1_dir = keyrings_dir.join("v1");
669
8
        if v1_dir.exists() {
670
4
            tracing::debug!("Scanning for v1 keyrings in {}", v1_dir.display());
671
16
            if let Ok(mut entries) = tokio::fs::read_dir(&v1_dir).await {
672
20
                while let Ok(Some(entry)) = entries.next_entry().await {
673
4
                    let path = entry.path();
674

            
675
                    // Skip directories and non-.keyring files
676
8
                    if path.is_dir() || path.extension() != Some(std::ffi::OsStr::new("keyring")) {
677
                        continue;
678
                    }
679

            
680
12
                    if let Some(name) = path.file_stem().and_then(|s| s.to_str()) {
681
8
                        tracing::debug!("Found v1 keyring: {name}");
682

            
683
                        // Try to load the keyring
684
20
                        match self.load_keyring(&path, name, secret.as_ref()).await {
685
4
                            Ok((name, label, alias, keyring)) => {
686
4
                                discovered.push((name, label, alias, keyring))
687
                            }
688
                            Err(e) => tracing::warn!("Failed to load keyring {:?}: {}", path, e),
689
                        }
690
                    }
691
                }
692
            }
693
        }
694

            
695
        // Scan for v0 keyrings
696
8
        if keyrings_dir.exists() {
697
4
            tracing::debug!("Scanning for v0 keyrings in {}", keyrings_dir.display());
698
16
            if let Ok(mut entries) = tokio::fs::read_dir(&keyrings_dir).await {
699
20
                while let Ok(Some(entry)) = entries.next_entry().await {
700
4
                    let path = entry.path();
701

            
702
                    // Skip directories and non-.keyring files
703
8
                    if path.is_dir() || path.extension() != Some(std::ffi::OsStr::new("keyring")) {
704
                        continue;
705
                    }
706

            
707
12
                    if let Some(name) = path.file_stem().and_then(|s| s.to_str()) {
708
8
                        tracing::debug!("Found v0 keyring: {name}");
709

            
710
                        // Try to load the keyring
711
20
                        match self.load_keyring(&path, name, secret.as_ref()).await {
712
4
                            Ok((name, label, alias, keyring)) => {
713
4
                                discovered.push((name, label, alias, keyring))
714
                            }
715
                            Err(e) => tracing::warn!("Failed to load keyring {:?}: {}", path, e),
716
                        }
717
                    }
718
                }
719
            }
720
        }
721

            
722
        // Discover KWallet keyrings for migration
723
        #[cfg(feature = "kwallet_migration")]
724
        self.discover_kwallet_keyrings(&self.data_dir, secret.as_ref(), &mut discovered)
725
            .await;
726

            
727
8
        let pending_count = self.pending_migrations.lock().await.len();
728

            
729
8
        if discovered.is_empty() && pending_count == 0 {
730
8
            tracing::info!("No keyrings discovered in data directory");
731
        } else {
732
2
            tracing::info!(
733
                "Discovered {} keyring(s), {pending_count} pending migration(s)",
734
                discovered.len(),
735
            );
736
        }
737

            
738
4
        Ok(discovered)
739
    }
740

            
741
    /// Discover KWallet keyrings for migration
742
    #[cfg(feature = "kwallet_migration")]
743
    async fn discover_kwallet_keyrings(
744
        &self,
745
        data_dir: &std::path::Path,
746
        secret: Option<&Secret>,
747
        discovered: &mut Vec<(String, String, String, Keyring)>,
748
    ) {
749
        let kwallet_dir = data_dir.join("kwalletd");
750

            
751
        if !kwallet_dir.exists() {
752
            tracing::debug!("No kwalletd directory found, skipping KWallet discovery");
753
            return;
754
        }
755

            
756
        tracing::debug!("Scanning for KWallet files in {}", kwallet_dir.display());
757

            
758
        let Ok(mut entries) = tokio::fs::read_dir(&kwallet_dir).await else {
759
            tracing::warn!("Failed to read kwalletd directory");
760
            return;
761
        };
762

            
763
        while let Ok(Some(entry)) = entries.next_entry().await {
764
            let path = entry.path();
765

            
766
            // Only process .kwl files
767
            if path.extension().is_none_or(|ext| ext != "kwl") {
768
                continue;
769
            }
770

            
771
            let Some(name) = path.file_stem().and_then(|s| s.to_str()) else {
772
                continue;
773
            };
774

            
775
            tracing::debug!("Found KWallet file: {name}");
776

            
777
            // Use lowercased name as alias
778
            let alias = name.to_lowercase();
779

            
780
            let label = {
781
                let mut chars = name.chars();
782
                match chars.next() {
783
                    None => String::new(),
784
                    Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
785
                }
786
            };
787

            
788
            let migration = PendingMigration::KWallet {
789
                name: name.to_owned(),
790
                path: path.clone(),
791
                label: label.clone(),
792
                alias: alias.clone(),
793
            };
794

            
795
            if let Some(secret) = secret {
796
                tracing::debug!("Attempting immediate migration of KWallet keyring '{name}'",);
797
                match migration.migrate(&self.data_dir, secret).await {
798
                    Ok(unlocked) => {
799
                        tracing::info!("Successfully migrated KWallet keyring '{name}' to oo7",);
800
                        discovered.push((
801
                            name.to_owned(),
802
                            label,
803
                            alias,
804
                            Keyring::Unlocked(unlocked),
805
                        ));
806
                        continue;
807
                    }
808
                    Err(e) => {
809
                        tracing::warn!(
810
                            "Failed to migrate KWallet keyring '{name}' at {}: {e}. Creating locked placeholder collection.",
811
                            migration.path().display()
812
                        );
813
                    }
814
                }
815
            }
816

            
817
            // Migration failed or no secret - create locked placeholder and register for
818
            // pending migration
819
            tracing::debug!(
820
                "Creating locked placeholder for KWallet keyring '{name}', will migrate on unlock",
821
            );
822

            
823
            match LockedKeyring::open_at(&self.data_dir, name).await {
824
                Ok(locked) => {
825
                    tracing::debug!(
826
                        "Created locked placeholder for '{name}', adding to pending migrations",
827
                    );
828
                    discovered.push((
829
                        name.to_owned(),
830
                        label.clone(),
831
                        alias.clone(),
832
                        Keyring::Locked(locked),
833
                    ));
834
                    self.pending_migrations
835
                        .lock()
836
                        .await
837
                        .insert(name.to_owned(), migration);
838
                }
839
                Err(e) => {
840
                    tracing::error!("Failed to create placeholder keyring for '{name}': {e}");
841
                }
842
            }
843
        }
844
    }
845

            
846
    /// Load a single keyring from a file path
847
    /// Returns (name, label, alias, keyring)
848
4
    async fn load_keyring(
849
        &self,
850
        path: &std::path::Path,
851
        name: &str,
852
        secret: Option<&Secret>,
853
    ) -> Result<(String, String, String, Keyring), Error> {
854
12
        let alias = if name.eq_ignore_ascii_case(Self::LOGIN_ALIAS) {
855
8
            oo7::dbus::Service::DEFAULT_COLLECTION.to_owned()
856
        } else {
857
8
            name.to_owned().to_lowercase()
858
        };
859

            
860
        // Use name as label (capitalized for consistency with Login)
861
        let label = {
862
8
            let mut chars = name.chars();
863
4
            match chars.next() {
864
                None => String::new(),
865
4
                Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
866
            }
867
        };
868

            
869
        // Try to load the keyring
870
16
        let keyring = match LockedKeyring::load(path).await {
871
4
            Ok(locked_keyring) => {
872
                // Successfully loaded as v1 keyring
873
8
                if let Some(secret) = secret {
874
8
                    match locked_keyring.unlock(secret.clone()).await {
875
4
                        Ok(unlocked) => {
876
8
                            tracing::info!("Unlocked keyring '{}' from {:?}", name, path);
877
4
                            Keyring::Unlocked(unlocked)
878
                        }
879
4
                        Err(e) => {
880
8
                            tracing::warn!(
881
                                "Failed to unlock keyring '{}' with provided secret: {}. Keeping it locked.",
882
                                name,
883
                                e
884
                            );
885
                            // Reload as locked since unlock consumed it
886
12
                            Keyring::Locked(LockedKeyring::load(path).await?)
887
                        }
888
                    }
889
                } else {
890
8
                    tracing::debug!("No secret provided, keeping keyring '{}' locked", name);
891
4
                    Keyring::Locked(locked_keyring)
892
                }
893
            }
894
8
            Err(oo7::file::Error::VersionMismatch(Some(version)))
895
8
                if version.first() == Some(&0) =>
896
            // v0 is the legacy version
897
            {
898
                // This is a v0 keyring that needs migration
899
8
                tracing::info!(
900
                    "Found legacy v0 keyring '{name}' at {}, registering for migration",
901
                    path.display()
902
                );
903

            
904
                let migration = PendingMigration::V0 {
905
4
                    name: name.to_owned(),
906
4
                    path: path.to_path_buf(),
907
4
                    label: label.clone(),
908
4
                    alias: alias.clone(),
909
                };
910

            
911
4
                if let Some(secret) = secret {
912
8
                    tracing::debug!("Attempting immediate migration of v0 keyring '{name}'",);
913
12
                    match UnlockedKeyring::open_at(&self.data_dir, name, secret.clone()).await {
914
4
                        Ok(unlocked) => {
915
8
                            tracing::info!("Successfully migrated v0 keyring '{name}' to v1",);
916

            
917
                            // Write the migrated keyring to disk
918
12
                            unlocked.write().await?;
919
4
                            tracing::info!("Wrote migrated keyring '{name}' to disk");
920

            
921
                            // Remove the v0 keyring file after successful migration
922
12
                            if let Err(e) = tokio::fs::remove_file(path).await {
923
                                tracing::warn!(
924
                                    "Failed to remove v0 keyring at {}: {e}",
925
                                    path.display()
926
                                );
927
                            } else {
928
8
                                tracing::info!("Removed v0 keyring file at {}", path.display());
929
                            }
930

            
931
4
                            return Ok((
932
4
                                name.to_owned(),
933
4
                                label,
934
4
                                alias,
935
4
                                Keyring::Unlocked(unlocked),
936
                            ));
937
                        }
938
4
                        Err(e) => {
939
8
                            tracing::warn!(
940
                                "Failed to migrate v0 keyring '{name}': {e}. Creating locked placeholder collection.",
941
                            );
942
                        }
943
                    }
944
                }
945

            
946
                // Migration failed or no secret - create locked placeholder and register for
947
                // pending migration
948
8
                tracing::debug!(
949
                    "Creating locked placeholder for v0 keyring '{}', will migrate on unlock",
950
                    name
951
                );
952

            
953
12
                let locked = LockedKeyring::open(name).await?;
954
16
                self.pending_migrations
955
                    .lock()
956
12
                    .await
957
4
                    .insert(name.to_owned(), migration);
958

            
959
4
                Keyring::Locked(locked)
960
            }
961
            Err(e) => {
962
                return Err(e.into());
963
            }
964
        };
965

            
966
8
        Ok((name.to_owned(), label, alias, keyring))
967
    }
968

            
969
    /// Initialize the service with collections and start client disconnect
970
    /// handler
971
29
    pub(crate) async fn initialize(
972
        &self,
973
        connection: zbus::Connection,
974
        mut discovered_keyrings: Vec<(String, String, String, Keyring)>, /* (name, label, alias,
975
                                                                          * keyring) */
976
        secret: Option<Secret>,
977
        auto_create_default: bool,
978
    ) -> Result<(), Error> {
979
56
        self.connection.set(connection.clone()).unwrap();
980

            
981
30
        let object_server = connection.object_server();
982
55
        let mut collections = self.collections.lock().await;
983

            
984
        // Check if we have a default collection
985
119
        let has_default = discovered_keyrings.iter().any(|(_, _, alias, _)| {
986
32
            alias == oo7::dbus::Service::DEFAULT_COLLECTION || alias == Self::LOGIN_ALIAS
987
        });
988

            
989
26
        if !has_default && auto_create_default {
990
            tracing::info!("No default collection found, creating 'Login' keyring");
991

            
992
            let keyring = if let Some(secret) = secret {
993
                UnlockedKeyring::open_at(&self.data_dir, Self::LOGIN_ALIAS, secret)
994
                    .await
995
                    .map(Keyring::Unlocked)
996
            } else {
997
                LockedKeyring::open_at(&self.data_dir, Self::LOGIN_ALIAS)
998
                    .await
999
                    .map(Keyring::Locked)
            };
            let keyring = keyring.inspect_err(|e| {
                tracing::error!("Failed to create default Login keyring: {}", e);
            })?;
            let is_locked = if keyring.is_locked() {
                "locked"
            } else {
                "unlocked"
            };
            discovered_keyrings.push((
                Self::LOGIN_ALIAS.to_owned(),
                "Login".to_owned(),
                oo7::dbus::Service::DEFAULT_COLLECTION.to_owned(),
                keyring,
            ));
            tracing::info!("Created default 'Login' collection ({})", is_locked);
        }
        // Set up discovered collections
115
        for (name, label, alias, keyring) in discovered_keyrings {
38
            let (unique_label, unique_alias) =
                Self::make_unique_label_and_alias(&collections, &label, &alias);
58
            let collection =
                Collection::new(&name, &unique_label, &unique_alias, self.clone(), keyring).await;
31
            collections.insert(collection.path().to_owned().into(), collection.clone());
96
            collection.dispatch_items().await?;
128
            object_server
31
                .at(collection.path(), collection.clone())
129
                .await?;
            // If this is the default collection, also register it at the alias path
32
            if unique_alias == oo7::dbus::Service::DEFAULT_COLLECTION {
166
                object_server
64
                    .at(DEFAULT_COLLECTION_ALIAS_PATH, collection)
127
                    .await?;
            }
        }
        // Always create session collection (always temporary)
        let collection = Collection::new(
            "session",
            "session",
32
            oo7::dbus::Service::SESSION_COLLECTION,
68
            self.clone(),
103
            Keyring::Unlocked(UnlockedKeyring::temporary(Secret::random().unwrap()).await?),
        )
98
        .await;
116
        object_server
63
            .at(collection.path(), collection.clone())
104
            .await?;
35
        collections.insert(collection.path().to_owned().into(), collection);
32
        drop(collections); // Release the lock
        // Spawn client disconnect handler
36
        let service = self.clone();
96
        tokio::spawn(async move { service.on_client_disconnect().await });
37
        Ok(())
    }
130
    async fn on_client_disconnect(&self) -> zbus::Result<()> {
164
        let rule = zbus::MatchRule::builder()
31
            .msg_type(zbus::message::Type::Signal)
            .sender("org.freedesktop.DBus")?
            .interface("org.freedesktop.DBus")?
            .member("NameOwnerChanged")?
            .arg(2, "")?
            .build();
65
        let mut stream = zbus::MessageStream::for_match_rule(rule, self.connection(), None).await?;
96
        while let Some(message) = stream.try_next().await? {
            let body = message.body();
            let Ok((_name, old_owner, new_owner)) =
                body.deserialize::<(String, Optional<UniqueName<'_>>, Optional<UniqueName<'_>>)>()
            else {
                continue;
            };
            debug_assert!(new_owner.is_none()); // We enforce that in the matching rule
            let old_owner = old_owner
                .as_ref()
                .expect("A disconnected client requires an old_owner");
            if let Some(session) = self.session_from_sender(old_owner).await {
                match session.close().await {
                    Ok(_) => tracing::info!(
                        "Client {} disconnected. Session: {} closed.",
                        old_owner,
                        session.path()
                    ),
                    Err(err) => tracing::error!("Failed to close session: {}", err),
                }
            }
        }
        Ok(())
    }
10
    pub async fn set_locked(
        &self,
        locked: bool,
        objects: &[OwnedObjectPath],
    ) -> Result<(Vec<OwnedObjectPath>, Vec<OwnedObjectPath>), ServiceError> {
10
        let mut without_prompt = Vec::new();
10
        let mut with_prompt = Vec::new();
20
        let collections = self.collections.lock().await;
40
        for object in objects {
31
            for (path, collection) in collections.iter() {
22
                let collection_locked = collection.is_locked().await;
11
                if *object == *path {
8
                    if collection_locked == locked {
6
                        tracing::debug!(
                            "Collection: {} is already {}.",
                            object,
                            if locked { "locked" } else { "unlocked" }
                        );
12
                        without_prompt.push(object.clone());
8
                    } else if locked {
                        // Locking never requires a prompt
26
                        collection.set_locked(true, None).await?;
8
                        without_prompt.push(object.clone());
                    } else {
                        // Unlocking may require a prompt
16
                        with_prompt.push(object.clone());
                    }
                    break;
24
                } else if let Some(item) = collection.item_from_path(object).await {
                    // If collection is locked, can't perform any item lock/unlock operations
8
                    if collection_locked {
                        // Unlocking an item when collection is locked requires unlocking collection
6
                        if !locked {
8
                            with_prompt.push(object.clone());
                        } else {
                            // Can't lock an item when collection is locked
4
                            return Err(ServiceError::IsLocked(format!(
                                "Cannot lock item {} when collection is locked",
                                object
                            )));
                        }
24
                    } else if locked == item.is_locked().await {
6
                        tracing::debug!(
                            "Item: {} is already {}.",
                            object,
                            if locked { "locked" } else { "unlocked" }
                        );
12
                        without_prompt.push(object.clone());
                    } else {
                        // Collection is unlocked, we can lock/unlock the item directly
24
                        let keyring = collection.keyring.read().await;
32
                        item.set_locked(locked, keyring.as_ref().unwrap().as_unlocked())
32
                            .await?;
8
                        without_prompt.push(object.clone());
                    }
                    break;
                }
14
                tracing::warn!("Object: {} does not exist.", object);
            }
        }
10
        Ok((without_prompt, with_prompt))
    }
34
    pub fn connection(&self) -> &zbus::Connection {
33
        self.connection.get().unwrap()
    }
31
    pub fn object_server(&self) -> &zbus::ObjectServer {
33
        self.connection().object_server()
    }
55
    pub async fn collection_from_path(&self, path: &ObjectPath<'_>) -> Option<Collection> {
27
        let collections = self.collections.lock().await;
27
        collections.get(path).cloned()
    }
126
    pub async fn session_index(&self) -> u32 {
63
        let n_sessions = *self.session_index.read().await + 1;
31
        *self.session_index.write().await = n_sessions;
32
        n_sessions
    }
    async fn session_from_sender(&self, sender: &UniqueName<'_>) -> Option<Session> {
        let sessions = self.sessions.lock().await;
        sessions.values().find(|s| s.sender() == sender).cloned()
    }
61
    pub async fn session(&self, path: &ObjectPath<'_>) -> Option<Session> {
29
        self.sessions.lock().await.get(path).cloned()
    }
24
    pub async fn remove_session(&self, path: &ObjectPath<'_>) {
12
        self.sessions.lock().await.remove(path);
    }
39
    pub async fn remove_collection(&self, path: &ObjectPath<'_>) {
22
        self.collections.lock().await.remove(path);
16
        if let Ok(signal_emitter) =
            self.signal_emitter(oo7::dbus::api::Service::PATH.as_deref().unwrap())
        {
16
            let _ = self.collections_changed(&signal_emitter).await;
        }
    }
44
    pub async fn prompt_index(&self) -> u32 {
22
        let n_prompts = *self.prompt_index.read().await + 1;
12
        *self.prompt_index.write().await = n_prompts;
10
        n_prompts
    }
44
    pub async fn prompt(&self, path: &ObjectPath<'_>) -> Option<Prompt> {
22
        self.prompts.lock().await.get(path).cloned()
    }
44
    pub async fn remove_prompt(&self, path: &ObjectPath<'_>) {
22
        self.prompts.lock().await.remove(path);
        // Also clean up pending collection if it exists
11
        self.pending_collections.lock().await.remove(path);
    }
16
    pub async fn register_prompt(&self, path: OwnedObjectPath, prompt: Prompt) {
8
        self.prompts.lock().await.insert(path, prompt);
    }
11
    pub async fn pending_collection(
        &self,
        prompt_path: &ObjectPath<'_>,
    ) -> Option<(String, String)> {
44
        self.pending_collections
            .lock()
27
            .await
9
            .get(prompt_path)
            .cloned()
    }
9
    pub async fn create_collection_with_secret(
        &self,
        label: &str,
        alias: &str,
        secret: Secret,
    ) -> Result<OwnedObjectPath, ServiceError> {
        // Create a persistent keyring with the provided secret
76
        let keyring = UnlockedKeyring::open_at(&self.data_dir, &label.to_lowercase(), secret)
30
            .await
24
            .map_err(|err| custom_service_error(&format!("Failed to create keyring: {err}")))?;
        // Write the keyring file to disk immediately
65
        keyring
            .write()
47
            .await
14
            .map_err(|err| custom_service_error(&format!("Failed to write keyring file: {err}")))?;
14
        let keyring = Keyring::Unlocked(keyring);
14
        let name = label.to_lowercase();
        // Create the collection with unique label and alias
14
        let (unique_label, unique_alias) = {
28
            let collections = self.collections.lock().await;
28
            Self::make_unique_label_and_alias(&collections, label, alias)
        };
35
        let collection =
            Collection::new(&name, &unique_label, &unique_alias, self.clone(), keyring).await;
26
        let collection_path: OwnedObjectPath = collection.path().to_owned().into();
        // Register with object server
48
        self.object_server()
12
            .at(collection.path(), collection.clone())
36
            .await?;
        // Add to collections
56
        self.collections
            .lock()
42
            .await
14
            .insert(collection_path.clone(), collection);
        // Emit CollectionCreated signal
14
        let service_path = oo7::dbus::api::Service::PATH.as_ref().unwrap();
14
        let signal_emitter = self.signal_emitter(service_path)?;
20
        Service::collection_created(&signal_emitter, &collection_path).await?;
        // Emit PropertiesChanged for Collections property to invalidate client cache
11
        self.collections_changed(&signal_emitter).await?;
8
        tracing::info!(
            "Collection `{}` created with label '{}'",
            collection_path,
            label
        );
11
        Ok(collection_path)
    }
9
    pub async fn complete_collection_creation(
        &self,
        prompt_path: &ObjectPath<'_>,
        secret: Secret,
    ) -> Result<OwnedObjectPath, ServiceError> {
18
        let Some((label, alias)) = self.pending_collection(prompt_path).await else {
8
            return Err(ServiceError::NoSuchObject(format!(
                "No pending collection for prompt `{prompt_path}`"
            )));
        };
46
        let collection_path = self
20
            .create_collection_with_secret(&label, &alias, secret)
42
            .await?;
24
        self.pending_collections.lock().await.remove(prompt_path);
11
        Ok(collection_path)
    }
62
    pub fn signal_emitter<'a, P>(
        &self,
        path: P,
    ) -> Result<zbus::object_server::SignalEmitter<'a>, oo7::dbus::ServiceError>
    where
        P: TryInto<ObjectPath<'a>>,
        P::Error: Into<zbus::Error>,
    {
120
        let signal_emitter = zbus::object_server::SignalEmitter::new(self.connection(), path)?;
54
        Ok(signal_emitter)
    }
    /// Extract the collection label from a list of object paths
    /// The objects can be either collections or items
32
    async fn extract_label_from_objects(&self, objects: &[OwnedObjectPath]) -> String {
16
        if objects.is_empty() {
            return String::new();
        }
        // Check if at least one of the objects is a Collection
32
        for object in objects {
24
            if let Some(collection) = self.collection_from_path(object).await {
16
                return collection.label().await;
            }
        }
        // Get the collection path from the first item
        // assumes all items are from the same collection
12
        if let Some(path_str) = objects.first().and_then(|p| p.as_str().rsplit_once('/')) {
4
            let collection_path = path_str.0;
12
            if let Ok(obj_path) = ObjectPath::try_from(collection_path)
8
                && let Some(collection) = self.collection_from_path(&obj_path).await
            {
8
                return collection.label().await;
            }
        }
        String::new()
    }
    /// Extract the collection from a list of object paths
    /// The objects can be either collections or items
8
    async fn extract_collection_from_objects(
        &self,
        objects: &[OwnedObjectPath],
    ) -> Option<Collection> {
16
        if objects.is_empty() {
            return None;
        }
        // Check if at least one of the objects is a Collection
32
        for object in objects {
24
            if let Some(collection) = self.collection_from_path(object).await {
8
                return Some(collection);
            }
        }
        // Get the collection path from the first item
        // (assumes all items are from the same collection)
12
        let path = objects
            .first()
            .unwrap()
            .as_str()
            .rsplit_once('/')
8
            .map(|(parent, _)| parent)?;
12
        self.collection_from_path(&ObjectPath::try_from(path).unwrap())
8
            .await
    }
    /// Attempt to migrate pending keyrings with the provided secret
    /// Returns a list of successfully migrated keyring names
28
    pub async fn migrate_pending_keyrings(&self, secret: &Secret) -> Vec<String> {
4
        let mut migrated = Vec::new();
8
        let mut pending = self.pending_migrations.lock().await;
4
        let mut to_remove = Vec::new();
16
        for (name, migration) in pending.iter() {
8
            tracing::debug!("Attempting to migrate pending keyring: {name}");
16
            match migration.migrate(&self.data_dir, secret).await {
4
                Ok(unlocked) => {
4
                    let label = migration.label();
4
                    let alias = migration.alias();
                    // Create a collection for this migrated keyring with unique label and alias
4
                    let (unique_label, unique_alias) = {
8
                        let collections = self.collections.lock().await;
8
                        Self::make_unique_label_and_alias(&collections, label, alias)
                    };
4
                    let keyring = Keyring::Unlocked(unlocked);
12
                    let collection =
                        Collection::new(name, &unique_label, &unique_alias, self.clone(), keyring)
20
                            .await;
4
                    let collection_path: OwnedObjectPath = collection.path().to_owned().into();
                    // Dispatch items
12
                    if let Err(e) = collection.dispatch_items().await {
                        tracing::error!(
                            "Failed to dispatch items for migrated keyring '{name}': {e}",
                        );
                        continue;
                    }
16
                    if let Err(e) = self
                        .object_server()
4
                        .at(collection.path(), collection.clone())
16
                        .await
                    {
                        tracing::error!(
                            "Failed to register migrated collection '{name}' with object server: {e}",
                        );
                        continue;
                    }
20
                    self.collections
                        .lock()
16
                        .await
8
                        .insert(collection_path.clone(), collection.clone());
4
                    if alias == oo7::dbus::Service::DEFAULT_COLLECTION
                        && let Err(e) = self
                            .object_server()
                            .at(DEFAULT_COLLECTION_ALIAS_PATH, collection)
                            .await
                    {
                        tracing::error!(
                            "Failed to register default alias for migrated collection '{name}': {e}",
                        );
                    }
8
                    if let Ok(signal_emitter) =
                        self.signal_emitter(oo7::dbus::api::Service::PATH.as_ref().unwrap())
                    {
8
                        let _ =
                            Service::collection_created(&signal_emitter, &collection_path).await;
12
                        let _ = self.collections_changed(&signal_emitter).await;
                    }
8
                    tracing::info!("Migrated keyring '{name}' added as collection",);
8
                    migrated.push(name.clone());
4
                    to_remove.push(name.clone());
                }
                Err(e) => {
                    tracing::debug!(
                        "Failed to migrate keyring '{name}' found at {} with provided secret: {e}",
                        migration.path().display()
                    );
                }
            }
        }
4
        for name in &to_remove {
8
            pending.remove(name);
        }
4
        migrated
    }
}
#[cfg(test)]
mod tests;