firebase_rs_sdk/app/
api.rs

1use std::collections::HashMap;
2use std::sync::{Arc, LazyLock, Mutex, MutexGuard};
3
4use crate::app::component::{Component, ComponentContainer, ComponentType};
5use crate::app::constants::{DEFAULT_ENTRY_NAME, PLATFORM_LOG_STRING};
6use crate::app::ensure_core_components_registered;
7use crate::app::errors::{AppError, AppResult};
8use crate::app::logger::{self, LogCallback, LogLevel, LogOptions, LOGGER};
9use crate::app::registry::{self, apps_guard, registered_components_guard, server_apps_guard};
10use crate::app::types::{
11    deep_equal_config, deep_equal_options, get_default_app_config, FirebaseApp, FirebaseAppConfig,
12    FirebaseAppSettings, FirebaseOptions, FirebaseServerApp, FirebaseServerAppSettings,
13    VersionService,
14};
15use crate::app::types::{is_browser, is_web_worker};
16use crate::component::types::{DynService, InstanceFactory, InstantiationMode};
17use sha2::{Digest, Sha256};
18
19pub static SDK_VERSION: &str = env!("CARGO_PKG_VERSION");
20
21static REGISTERED_VERSIONS: LazyLock<Mutex<HashMap<String, String>>> =
22    LazyLock::new(|| Mutex::new(HashMap::new()));
23
24static GLOBAL_APP_LOCK: LazyLock<Mutex<()>> = LazyLock::new(|| Mutex::new(()));
25
26fn global_app_guard() -> MutexGuard<'static, ()> {
27    GLOBAL_APP_LOCK
28        .lock()
29        .unwrap_or_else(|poison| poison.into_inner())
30}
31
32fn merged_settings(raw: Option<FirebaseAppSettings>) -> FirebaseAppSettings {
33    raw.unwrap_or_default()
34}
35
36fn normalize_name(settings: &FirebaseAppSettings) -> AppResult<String> {
37    let name = settings
38        .name
39        .clone()
40        .unwrap_or_else(|| DEFAULT_ENTRY_NAME.to_string());
41    if name.trim().is_empty() {
42        return Err(AppError::BadAppName { app_name: name });
43    }
44    Ok(name)
45}
46
47fn automatic_data_collection(settings: &FirebaseAppSettings) -> bool {
48    settings.automatic_data_collection_enabled.unwrap_or(true)
49}
50
51fn ensure_options(mut options: FirebaseOptions) -> AppResult<FirebaseOptions> {
52    if !options_are_defined(&options) {
53        if let Some(defaults) = get_default_app_config() {
54            options = defaults;
55        }
56    }
57
58    if !options_are_defined(&options) {
59        return Err(AppError::NoOptions);
60    }
61
62    Ok(options)
63}
64
65fn options_are_defined(options: &FirebaseOptions) -> bool {
66    options.api_key.is_some()
67        || options.project_id.is_some()
68        || options.app_id.is_some()
69        || options.auth_domain.is_some()
70        || options.database_url.is_some()
71        || options.storage_bucket.is_some()
72        || options.messaging_sender_id.is_some()
73        || options.measurement_id.is_some()
74}
75
76fn validate_token_ttl(token: Option<&str>, token_name: &str) {
77    use base64::engine::general_purpose::STANDARD;
78    use base64::Engine;
79
80    let Some(token) = token else {
81        return;
82    };
83
84    let parts: Vec<&str> = token.split('.').collect();
85    if parts.len() < 2 {
86        LOGGER.warn(format!(
87            "FirebaseServerApp {token_name} is invalid: second part could not be parsed."
88        ));
89        return;
90    }
91
92    let Ok(decoded) = STANDARD.decode(parts[1]) else {
93        LOGGER.warn(format!(
94            "FirebaseServerApp {token_name} is invalid: second part could not be parsed."
95        ));
96        return;
97    };
98
99    let Ok(claims) = serde_json::from_slice::<serde_json::Value>(&decoded) else {
100        LOGGER.warn(format!(
101            "FirebaseServerApp {token_name} is invalid: expiration claim could not be parsed"
102        ));
103        return;
104    };
105
106    let exp_ms = claims
107        .get("exp")
108        .and_then(|value| value.as_i64())
109        .map(|seconds| seconds * 1000);
110
111    let Some(exp) = exp_ms else {
112        LOGGER.warn(format!(
113            "FirebaseServerApp {token_name} is invalid: expiration claim could not be parsed"
114        ));
115        return;
116    };
117
118    let now = chrono::Utc::now().timestamp_millis();
119    if exp <= now {
120        LOGGER.warn(format!(
121            "FirebaseServerApp {token_name} is invalid: the token has expired."
122        ));
123    }
124}
125
126fn supports_finalization_registry() -> bool {
127    false
128}
129
130fn server_app_hash(options: &FirebaseOptions, settings: &FirebaseServerAppSettings) -> String {
131    let mut hasher = Sha256::new();
132
133    fn write_option(hasher: &mut Sha256, value: &Option<String>) {
134        if let Some(v) = value {
135            hasher.update(v.as_bytes());
136        }
137        hasher.update([0]);
138    }
139
140    write_option(&mut hasher, &options.api_key);
141    write_option(&mut hasher, &options.auth_domain);
142    write_option(&mut hasher, &options.database_url);
143    write_option(&mut hasher, &options.project_id);
144    write_option(&mut hasher, &options.storage_bucket);
145    write_option(&mut hasher, &options.messaging_sender_id);
146    write_option(&mut hasher, &options.app_id);
147    write_option(&mut hasher, &options.measurement_id);
148
149    write_option(
150        &mut hasher,
151        &settings
152            .automatic_data_collection_enabled
153            .map(|value| value.to_string()),
154    );
155    write_option(&mut hasher, &settings.auth_id_token);
156    write_option(&mut hasher, &settings.app_check_token);
157
158    let digest = hasher.finalize();
159    format!("serverapp-{digest:x}")
160}
161
162/// Creates (or returns) a `FirebaseApp` instance for the provided options and settings.
163///
164/// When an app with the same normalized name already exists, the existing instance is
165/// returned as long as the configuration matches. A mismatch results in `AppError::DuplicateApp`.
166pub fn initialize_app(
167    options: FirebaseOptions,
168    settings: Option<FirebaseAppSettings>,
169) -> AppResult<FirebaseApp> {
170    ensure_core_components_registered();
171    let _guard = global_app_guard();
172    let settings = merged_settings(settings);
173    let name = normalize_name(&settings)?;
174    let automatic = automatic_data_collection(&settings);
175
176    let options = ensure_options(options)?;
177
178    let config = FirebaseAppConfig::new(name.clone(), automatic);
179
180    {
181        let apps = apps_guard();
182        if let Some(existing) = apps.get(&name) {
183            if deep_equal_options(&options, &existing.options())
184                && deep_equal_config(&config, &existing.config())
185            {
186                return Ok(existing.clone());
187            } else {
188                return Err(AppError::DuplicateApp { app_name: name });
189            }
190        }
191    }
192
193    let container = ComponentContainer::new(name.clone());
194
195    let components: Vec<Component> = {
196        let global = registered_components_guard();
197        global.values().cloned().collect()
198    };
199
200    let app = FirebaseApp::new(options.clone(), config.clone(), container.clone());
201
202    let app_for_factory = app.clone();
203    let app_factory: InstanceFactory =
204        Arc::new(move |_container, _options| Ok(Arc::new(app_for_factory.clone()) as DynService));
205    let _ = container.add_component(Component::new("app", app_factory, ComponentType::Public));
206    for component in components {
207        let _ = container.add_component(component);
208    }
209
210    apps_guard().insert(name.clone(), app.clone());
211
212    Ok(app)
213}
214
215/// Retrieves a previously initialized `FirebaseApp` by name.
216///
217/// Passing `None` looks up the default app entry.
218pub fn get_app(name: Option<&str>) -> AppResult<FirebaseApp> {
219    ensure_core_components_registered();
220    let _guard = global_app_guard();
221    let lookup = name.unwrap_or(DEFAULT_ENTRY_NAME);
222    if let Some(app) = apps_guard().get(lookup) {
223        return Ok(app.clone());
224    }
225    Err(AppError::NoApp {
226        app_name: lookup.to_string(),
227    })
228}
229
230/// Returns a snapshot of all registered `FirebaseApp` instances.
231pub fn get_apps() -> Vec<FirebaseApp> {
232    ensure_core_components_registered();
233    let _guard = global_app_guard();
234    apps_guard().values().cloned().collect()
235}
236
237/// Deletes the provided `FirebaseApp` from the global registry and tears down services.
238pub fn delete_app(app: &FirebaseApp) -> AppResult<()> {
239    let _guard = global_app_guard();
240    let name = app.name().to_string();
241    let removed = apps_guard().remove(&name);
242
243    if removed.is_some() {
244        for provider in app.container().get_providers() {
245            let _ = provider.delete();
246        }
247        app.set_is_deleted(true);
248    }
249
250    Ok(())
251}
252
253/// Creates or reuses a server-side `FirebaseServerApp` instance from options and settings.
254pub fn initialize_server_app(
255    options: Option<FirebaseOptions>,
256    settings: Option<FirebaseServerAppSettings>,
257) -> AppResult<FirebaseServerApp> {
258    ensure_core_components_registered();
259
260    if is_browser() && !is_web_worker() {
261        return Err(AppError::InvalidServerAppEnvironment);
262    }
263
264    let mut server_settings = settings.unwrap_or_default();
265    if server_settings.automatic_data_collection_enabled.is_none() {
266        server_settings.automatic_data_collection_enabled = Some(true);
267    }
268
269    let app_options = match options.or_else(get_default_app_config) {
270        Some(opts) => opts,
271        None => return Err(AppError::NoOptions),
272    };
273
274    validate_token_ttl(server_settings.auth_id_token.as_deref(), "authIdToken");
275    validate_token_ttl(server_settings.app_check_token.as_deref(), "appCheckToken");
276
277    if server_settings.release_on_deref.is_some() && !supports_finalization_registry() {
278        return Err(AppError::FinalizationRegistryNotSupported);
279    }
280
281    let name = server_app_hash(&app_options, &server_settings);
282
283    let container = ComponentContainer::new(name.clone());
284    for component in registered_components_guard().values() {
285        let _ = container.add_component(component.clone());
286    }
287
288    let base_app = FirebaseApp::new(
289        app_options.clone(),
290        FirebaseAppConfig::new(
291            name.clone(),
292            server_settings
293                .automatic_data_collection_enabled
294                .unwrap_or(true),
295        ),
296        container.clone(),
297    );
298
299    let base_for_factory = base_app.clone();
300    let app_factory: InstanceFactory =
301        Arc::new(move |_container, _| Ok(Arc::new(base_for_factory.clone()) as DynService));
302    let _ = container.add_component(Component::new("app", app_factory, ComponentType::Public));
303
304    let server_app = FirebaseServerApp::new(base_app, server_settings.clone());
305
306    {
307        let _guard = global_app_guard();
308        if let Some(existing) = server_apps_guard().get(&name) {
309            existing.inc_ref_count();
310            return Ok(existing.clone());
311        }
312
313        server_apps_guard().insert(name.clone(), server_app.clone());
314    }
315
316    register_version("@firebase/app", SDK_VERSION, Some("serverapp"));
317
318    Ok(server_app)
319}
320
321/// Registers a library version component so it can be queried by other Firebase services.
322pub fn register_version(library: &str, version: &str, variant: Option<&str>) {
323    let _guard = global_app_guard();
324    let mut library_key = PLATFORM_LOG_STRING
325        .get(library)
326        .copied()
327        .unwrap_or(library)
328        .to_string();
329    if let Some(variant) = variant {
330        library_key.push('-');
331        library_key.push_str(variant);
332    }
333
334    if library_key.contains([' ', '/']) || version.contains([' ', '/']) {
335        LOGGER.warn(format!(
336            "Unable to register library '{library_key}' with version '{version}': contains illegal characters"
337        ));
338        return;
339    }
340
341    REGISTERED_VERSIONS
342        .lock()
343        .unwrap_or_else(|poison| poison.into_inner())
344        .insert(library_key.clone(), version.to_string());
345
346    let component_name = format!("{library_key}-version");
347    let version_string = version.to_string();
348    let library_string = library_key.clone();
349    let factory: InstanceFactory = Arc::new(move |_, _| {
350        let service = VersionService {
351            library: library_string.clone(),
352            version: version_string.clone(),
353        };
354        Ok(Arc::new(service) as DynService)
355    });
356
357    let component = Component::new(component_name, factory, ComponentType::Version)
358        .with_instantiation_mode(InstantiationMode::Eager);
359    let _ = registry::register_component(component);
360}
361
362#[cfg(test)]
363pub(crate) fn clear_registered_versions_for_tests() {
364    REGISTERED_VERSIONS
365        .lock()
366        .unwrap_or_else(|poison| poison.into_inner())
367        .clear();
368}
369
370/// Installs a user-supplied logger that receives Firebase diagnostic messages.
371pub fn on_log(callback: Option<LogCallback>, options: Option<LogOptions>) -> AppResult<()> {
372    logger::set_user_log_handler(callback, options);
373    Ok(())
374}
375
376/// Sets the global Firebase SDK log level.
377pub fn set_log_level(level: LogLevel) {
378    let _ = logger::set_log_level(level);
379}
380
381#[cfg(test)]
382mod tests {
383    use super::*;
384    use crate::app::heartbeat::clear_heartbeat_store_for_tests;
385    use crate::app::registry;
386    use crate::component::types::{ComponentType, DynService, InstanceFactory, InstantiationMode};
387    use crate::component::Component;
388    use std::sync::atomic::{AtomicUsize, Ordering};
389    use std::sync::{Arc, LazyLock, Mutex};
390
391    static TEST_COUNTER: AtomicUsize = AtomicUsize::new(0);
392    static TEST_SERIAL: LazyLock<Mutex<()>> = LazyLock::new(|| Mutex::new(()));
393
394    fn next_name(prefix: &str) -> String {
395        let id = TEST_COUNTER.fetch_add(1, Ordering::SeqCst);
396        format!("{}-{}", prefix, id)
397    }
398
399    fn test_options() -> FirebaseOptions {
400        FirebaseOptions {
401            api_key: Some("test-key".to_string()),
402            project_id: Some("test-project".to_string()),
403            app_id: Some("1:123:web:test".to_string()),
404            ..Default::default()
405        }
406    }
407
408    fn reset() {
409        {
410            let _guard = super::global_app_guard();
411            let mut apps = registry::apps_guard();
412            for app in apps.values() {
413                app.set_is_deleted(true);
414            }
415            apps.clear();
416            registry::server_apps_guard().clear();
417        }
418
419        assert!(registry::apps_guard().is_empty());
420        crate::component::clear_global_components_for_test();
421        clear_registered_versions_for_tests();
422        clear_heartbeat_store_for_tests();
423    }
424
425    fn with_serialized_test<F: FnOnce()>(f: F) {
426        let _guard = TEST_SERIAL.lock().unwrap();
427        reset();
428        f();
429    }
430
431    fn make_test_component(name: &str) -> Component {
432        let factory: InstanceFactory = Arc::new(|_, _| Ok(Arc::new(()) as DynService));
433        Component::new(name.to_string(), factory, ComponentType::Public)
434            .with_instantiation_mode(InstantiationMode::Lazy)
435    }
436
437    #[test]
438    fn initialize_app_creates_default_app() {
439        with_serialized_test(|| {
440            let app = super::initialize_app(test_options(), None).expect("init app");
441            assert_eq!(app.name(), DEFAULT_ENTRY_NAME);
442        });
443    }
444
445    #[test]
446    fn initialize_app_creates_named_app() {
447        with_serialized_test(|| {
448            let app = super::initialize_app(
449                test_options(),
450                Some(FirebaseAppSettings {
451                    name: Some("MyApp".to_string()),
452                    automatic_data_collection_enabled: None,
453                }),
454            )
455            .expect("init named app");
456            assert_eq!(app.name(), "MyApp");
457        });
458    }
459
460    #[test]
461    fn initialize_app_with_same_options_returns_same_instance() {
462        with_serialized_test(|| {
463            let opts = test_options();
464            let app1 = super::initialize_app(opts.clone(), None).expect("first init");
465            let app2 = super::initialize_app(opts, None).expect("second init");
466            let container1 = app1.container().inner.clone();
467            let container2 = app2.container().inner.clone();
468            assert!(Arc::ptr_eq(&container1, &container2));
469        });
470    }
471
472    #[test]
473    fn initialize_app_duplicate_options_fails() {
474        with_serialized_test(|| {
475            let app_name = next_name("dup-app");
476            let opts1 = test_options();
477            let settings = FirebaseAppSettings {
478                name: Some(app_name.clone()),
479                automatic_data_collection_enabled: None,
480            };
481            let _ =
482                super::initialize_app(opts1.clone(), Some(settings.clone())).expect("first init");
483            let mut opts2 = opts1.clone();
484            opts2.api_key = Some("other-key".to_string());
485            let result = super::initialize_app(opts2, Some(settings));
486            assert!(matches!(result, Err(AppError::DuplicateApp { .. })));
487        });
488    }
489
490    #[test]
491    fn initialize_app_duplicate_config_fails() {
492        with_serialized_test(|| {
493            let opts = test_options();
494            let settings = FirebaseAppSettings {
495                name: Some("dup".to_string()),
496                automatic_data_collection_enabled: Some(true),
497            };
498            let _ =
499                super::initialize_app(opts.clone(), Some(settings.clone())).expect("first init");
500            let mut other = settings.clone();
501            other.automatic_data_collection_enabled = Some(false);
502            let result = super::initialize_app(opts, Some(other));
503            assert!(matches!(result, Err(AppError::DuplicateApp { .. })));
504        });
505    }
506
507    #[test]
508    fn automatic_data_collection_defaults_true() {
509        with_serialized_test(|| {
510            let app = super::initialize_app(test_options(), None).expect("init app");
511            assert!(app.automatic_data_collection_enabled());
512        });
513    }
514
515    #[test]
516    fn automatic_data_collection_respects_setting() {
517        with_serialized_test(|| {
518            let app = super::initialize_app(
519                test_options(),
520                Some(FirebaseAppSettings {
521                    name: None,
522                    automatic_data_collection_enabled: Some(false),
523                }),
524            )
525            .expect("init app");
526            assert!(!app.automatic_data_collection_enabled());
527        });
528    }
529
530    #[test]
531    fn registered_components_attach_to_new_app() {
532        with_serialized_test(|| {
533            let name1 = next_name("test-component");
534            let name2 = next_name("test-component");
535            let _ = registry::register_component(make_test_component(&name1));
536            let _ = registry::register_component(make_test_component(&name2));
537
538            let app = super::initialize_app(test_options(), None).expect("init app");
539            assert!(app.container().get_provider(&name1).is_component_set());
540            assert!(app.container().get_provider(&name2).is_component_set());
541        });
542    }
543
544    #[test]
545    fn delete_app_marks_app_deleted_and_clears_registry() {
546        with_serialized_test(|| {
547            let app = super::initialize_app(test_options(), None).expect("init app");
548            let name = app.name().to_string();
549            {
550                let apps = registry::apps_guard();
551                assert!(apps.contains_key(&name));
552            }
553            assert!(super::delete_app(&app).is_ok());
554            assert!(app.is_deleted());
555            {
556                let apps = registry::apps_guard();
557                assert!(!apps.contains_key(&name));
558            }
559        });
560    }
561
562    #[test]
563    fn register_version_registers_component() {
564        with_serialized_test(|| {
565            let library = next_name("lib");
566            super::register_version(&library, "1.0.0", None);
567            let components = registry::registered_components_guard();
568            let expected = format!("{}-version", library);
569            assert!(components.keys().any(|key| key.as_ref() == expected));
570        });
571    }
572
573    #[test]
574    fn get_app_returns_existing_app() {
575        with_serialized_test(|| {
576            let created = super::initialize_app(test_options(), None).expect("init app");
577            let fetched = super::get_app(None).expect("get app");
578            assert_eq!(created.name(), fetched.name());
579        });
580    }
581
582    #[test]
583    fn get_app_nonexistent_fails() {
584        with_serialized_test(|| {
585            let result = super::get_app(Some("missing"));
586            assert!(matches!(result, Err(AppError::NoApp { .. })));
587        });
588    }
589}