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
162pub 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
215pub 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
230pub fn get_apps() -> Vec<FirebaseApp> {
232 ensure_core_components_registered();
233 let _guard = global_app_guard();
234 apps_guard().values().cloned().collect()
235}
236
237pub 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
253pub 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
321pub 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
370pub fn on_log(callback: Option<LogCallback>, options: Option<LogOptions>) -> AppResult<()> {
372 logger::set_user_log_handler(callback, options);
373 Ok(())
374}
375
376pub 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}