/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ use error_support::handle_error; use parking_lot::{Mutex, MutexGuard, RwLock, RwLockReadGuard}; use crate::container::Container; use crate::data::{ContainersData, Identity, MAX_USER_CONTEXT_ID}; use crate::defaults::{self, DefaultIdentity}; use crate::definitions::{ContainerColor, ContainerIcon}; use crate::error::{InitError, StoreError}; use crate::format::{parse, serialize}; /// How the store reaches the outside world. /// /// The callback may call `serialize`, but mutating the store from it deadlocks /// on the callback lock. Mutate on a later turn. #[uniffi::export(callback_interface)] pub trait ContainersCallback: Send + Sync { fn persist(&self); } struct NoopCallback; impl ContainersCallback for NoopCallback { fn persist(&self) {} } #[derive(Clone, Debug, PartialEq, Eq, uniffi::Record)] pub struct SiteAssociation { pub site: String, pub user_context_id: u32, } #[derive(uniffi::Object)] pub struct ContainersStore { data: Mutex, default_identities: Vec, callback: RwLock>, } #[uniffi::export] impl ContainersStore { /// Seeds the store from a stored document, or from the default identities /// when `bytes` is `None`. If a migration ran, the document is persisted /// before this returns. /// /// `default_identities` replaces the shipped public identities, for /// embedders that let enterprise policy define them. They are consulted /// whether or not there is a stored document: the label of a default /// container the user has not renamed comes from them, since it is not /// stored. #[uniffi::constructor] #[handle_error(InitError)] pub fn new( bytes: Option>, default_identities: Option>, callback: Box, ) -> Result { let default_identities = default_identities.unwrap_or_else(defaults::shipped_defaults); let (data, migrated) = match bytes { Some(bytes) => parse(&bytes)?, None => (defaults::defaults_with(&default_identities), true), }; let store = Self { data: Mutex::new(data), default_identities, callback: RwLock::new(callback), }; if migrated { store.persist(); } Ok(store) } /// Drops the callback, so that a late mutation during teardown cannot reach /// an embedder that is already gone. /// /// One way: there is no putting it back. From here on the store keeps /// working in memory but persists nothing. pub fn unset_callback(&self) { *self.callback.write() = Box::new(NoopCallback); } /// The document as it stands, for the embedder to write. pub fn serialize(&self) -> Vec { serialize(&self.data()) } pub fn public_identities(&self) -> Vec { self.data() .public_identities() .map(|identity| self.container(identity)) .collect() } pub fn public_user_context_ids(&self) -> Vec { self.data() .public_identities() .map(|identity| identity.user_context_id) .collect() } pub fn private_user_context_ids(&self) -> Vec { self.data() .private_identities() .map(|identity| identity.user_context_id) .collect() } pub fn public_identity_from_id(&self, user_context_id: u32) -> Option { self.data() .identities .iter() .find(|identity| identity.public && identity.user_context_id == user_context_id) .map(|identity| self.container(identity)) } pub fn private_identity(&self, name: &str) -> Option { self.data() .find_private_by_name(name) .map(|identity| self.container(identity)) } #[handle_error(StoreError)] pub fn create( &self, name: &str, icon: ContainerIcon, color: ContainerColor, ) -> Result { if name.trim().is_empty() { return Err(StoreError::EmptyName); } let identity = { let mut data = self.data(); let identity = Identity { user_context_id: next_user_context_id(&mut data)?, public: true, icon: icon.name().to_string(), color: color.name().to_string(), name: Some(name.to_string()), policy: false, policy_id: None, extra: Default::default(), }; data.identities.push(identity.clone()); identity }; self.persist(); Ok(self.container(&identity)) } #[handle_error(StoreError)] pub fn create_for_policy(&self, policy_id: &str) -> Result { if policy_id.trim().is_empty() { return Err(StoreError::EmptyPolicyId); } let identity = { let mut data = self.data(); let identity = Identity { user_context_id: next_user_context_id(&mut data)?, public: false, icon: String::new(), color: String::new(), name: Some(policy_id.to_string()), policy: true, policy_id: Some(policy_id.to_string()), extra: Default::default(), }; data.identities.push(identity.clone()); identity }; self.persist(); Ok(self.container(&identity)) } pub fn policy_identities(&self) -> Vec { self.data() .policy_identities() .map(|identity| self.container(identity)) .collect() } pub fn policy_identity(&self, policy_id: &str) -> Option { self.data() .find_policy_by_id(policy_id) .map(|identity| self.container(identity)) } pub fn remove_policy_identity(&self, user_context_id: u32) -> Option { let identity = { let mut data = self.data(); let index = data.identities.iter().position(|identity| { identity.policy && identity.user_context_id == user_context_id })?; // A site can only be bound to a public container, so there should be // nothing to drop here. Cheap enough to not depend on that. data.site_associations .retain(|_, id| *id != user_context_id); data.identities.remove(index) }; self.persist(); Some(self.container(&identity)) } #[handle_error(StoreError)] pub fn update( &self, user_context_id: u32, name: &str, icon: ContainerIcon, color: ContainerColor, ) -> Result, StoreError> { if name.trim().is_empty() { return Err(StoreError::EmptyName); } let identity = { let mut data = self.data(); let Some(identity) = data.identities.iter_mut().find(|identity| { identity.public && identity.user_context_id == user_context_id }) else { return Ok(None); }; identity.name = Some(name.to_string()); identity.icon = icon.name().to_string(); identity.color = color.name().to_string(); identity.clone() }; self.persist(); Ok(Some(self.container(&identity))) } /// Removes a public container. Clearing the data stored under its origin /// attributes is the embedder's job. pub fn remove(&self, user_context_id: u32) -> Option { let identity = { let mut data = self.data(); let index = data.identities.iter().position(|identity| { identity.public && identity.user_context_id == user_context_id })?; data.site_associations .retain(|_, id| *id != user_context_id); data.identities.remove(index) }; self.persist(); Some(self.container(&identity)) } /// Moves the given public containers before `position`, or to the end when /// it is -1. Positions below -1 are rejected. pub fn move_containers(&self, user_context_ids: Vec, position: i64) -> bool { if position < -1 { return false; } { let mut data = self.data(); let moved: Vec = data .identities .iter() .filter(|identity| { identity.public && user_context_ids.contains(&identity.user_context_id) }) .cloned() .collect(); if moved.is_empty() { return false; } let mut destination = if position == -1 { data.identities.len() as i64 } else { position }; // Skip over the private identities that sit before the destination. for (index, identity) in data.identities.iter().enumerate() { if !identity.public && destination >= index as i64 { destination += 1; } } data.identities.retain(|identity| { !identity.public || !user_context_ids.contains(&identity.user_context_id) }); let destination = (destination.max(0) as usize).min(data.identities.len()); for (offset, identity) in moved.into_iter().enumerate() { data.identities.insert(destination + offset, identity); } } self.persist(); true } #[handle_error(StoreError)] pub fn set_site_association(&self, site: &str, user_context_id: u32) -> Result<(), StoreError> { { let mut data = self.data(); if !data .identities .iter() .any(|identity| identity.public && identity.user_context_id == user_context_id) { return Err(StoreError::NoSuchContainer { user_context_id }); } let host = normalize_site(site).ok_or(StoreError::InvalidSite)?; if data.site_associations.get(&host) == Some(&user_context_id) { return Ok(()); } data.site_associations.insert(host, user_context_id); } self.persist(); Ok(()) } pub fn remove_site_association(&self, site: &str) { let Some(host) = normalize_site(site) else { return; }; if self.data().site_associations.remove(&host).is_none() { return; } self.persist(); } /// The container bound to `site`, or 0 when there is none. pub fn get_site_association(&self, site: &str) -> u32 { normalize_site(site) .and_then(|host| self.data().site_associations.get(&host).copied()) .unwrap_or(0) } pub fn get_site_associations(&self, user_context_id: Option) -> Vec { self.data() .site_associations .iter() .filter(|(_, id)| user_context_id.is_none_or(|wanted| **id == wanted)) .map(|(site, id)| SiteAssociation { site: site.clone(), user_context_id: *id, }) .collect() } } /// Reserves the next id, or reports that there is none left. The counter is /// shared by every kind of container, policy-owned ones included, so that an id /// is never handed out twice. fn next_user_context_id(data: &mut ContainersData) -> Result { // The reserved id is the last valid one, so it has to stay free. if data.last_user_context_id >= MAX_USER_CONTEXT_ID - 1 { return Err(StoreError::IdSpaceExhausted); } data.last_user_context_id += 1; Ok(data.last_user_context_id) } /// Not exported: these hand out lock guards, which have no meaning across the /// FFI boundary. impl ContainersStore { fn data(&self) -> MutexGuard<'_, ContainersData> { self.data.lock() } /// Mirrors getUserContextLabel: only a public container falls back to the /// label of the default identity that owns its id. fn container(&self, identity: &Identity) -> Container { let default_label = identity .public .then(|| defaults::default_label(&self.default_identities, identity.user_context_id)) .flatten(); Container::from_identity(identity, default_label) } fn callback(&self) -> RwLockReadGuard<'_, Box> { self.callback.read() } /// Always called with the data lock released, so that the callback is free /// to serialize. fn persist(&self) { self.callback().persist(); } } /// Lower cased, IDN-encoded host, or `None` when `site` cannot be bound to a /// container. #[uniffi::export] pub fn normalize_site(site: &str) -> Option { // Strict applies the STD3 ASCII rules, which is what keeps out everything // that is not a host: ports, paths, userinfo, percent escapes, brackets, // wildcards and whitespace. The lenient variant accepts all of them. // // It also rejects a trailing dot, which does name the DNS root and which // Gecko accepts, so validate without it and put it back. let (bare, root) = match site.strip_suffix('.') { Some(bare) => (bare, "."), None => (site, ""), }; let host = idna::domain_to_ascii_strict(bare).ok()?; Some(format!("{host}{root}")) }