lib2fas/
lib2fas.rs

1use crate::decrypt::decrypt_services;
2use crate::fuzzy::fuzzy_match;
3use crate::helpers::{calculate_hash, now};
4use crate::json5_support::{json5_from_reader, json5_from_reader_async};
5use core::fmt::Formatter;
6use core::hash::{Hash, Hasher};
7use serde::{Deserialize, Serialize, Serializer};
8use std::collections::{BTreeMap, VecDeque};
9use std::path::PathBuf;
10
11#[derive(Deserialize, Debug, Clone, PartialEq, PartialOrd, Ord, Eq, Hash, Default)]
12pub(crate) struct RawOTPDetails {
13    link: Option<String>,
14    #[serde(alias = "tokenType")]
15    token_type: Option<String>,
16    source: Option<String>,
17    label: Option<String>,
18    account: Option<String>,
19    digits: Option<i32>,
20    period: Option<i32>,
21}
22
23impl RawOTPDetails {
24    #[must_use]
25    pub fn totp_client(&self) -> Option<totp_rs::TOTP> {
26        let link = self.link.as_ref()?;
27
28        // TOTP::from_url checks the secret length for some reason, this version does not:
29        totp_rs::TOTP::from_url_unchecked(link).ok()
30    }
31}
32
33/// Represents details of a one-time password (OTP) entry, including metadata, configuration,
34/// and a TOTP client for generating time-based OTPs.
35///
36/// This struct is used to encapsulate the data and functionality associated with OTPs, including:
37/// - TOTP generation at the current time, previous, or future time intervals.
38/// - Conversion between string and numeric representations of tokens.
39#[derive(Serialize, Debug, Clone, Default)]
40pub struct OTPDetails {
41    /// The link associated with the OTP, typically containing configuration details in URL format.
42    link: Option<String>,
43    /// The type of the token (e.g., TOTP, HOTP).
44    token_type: Option<String>,
45    /// The source or origin of the OTP configuration.
46    source: Option<String>,
47    /// A human-readable label for the OTP entry.
48    label: Option<String>,
49    /// The account associated with this OTP.
50    account: Option<String>,
51    /// The number of digits in the generated OTP (e.g., 6 or 8).
52    digits: Option<i32>,
53    /// The period (in seconds) for generating time-based OTPs.
54    period: Option<i32>,
55    /// A TOTP client capable of generating time-based OTPs.
56    #[serde(skip_serializing)]
57    client: Option<totp_rs::TOTP>,
58}
59
60impl PartialEq for OTPDetails {
61    fn eq(
62        &self,
63        other: &Self,
64    ) -> bool {
65        // this ensures 'client' is omitted from eq check while other fields are checked
66        self.fields().eq(&other.fields())
67    }
68}
69
70// empty impl but required to make Rust understand that eq is possible based on previous partialeq
71impl Eq for OTPDetails {}
72
73impl Hash for OTPDetails {
74    fn hash<H: Hasher>(
75        &self,
76        state: &mut H,
77    ) {
78        self.fields().hash(state);
79        // exclude client!
80    }
81}
82
83impl From<RawOTPDetails> for OTPDetails {
84    fn from(val: RawOTPDetails) -> Self {
85        let client = val.totp_client();
86
87        Self {
88            client,
89            link: val.link,
90            token_type: val.token_type,
91            source: val.source,
92            label: val.label,
93            account: val.account,
94            digits: val.digits,
95            period: val.period,
96        }
97    }
98}
99
100impl OTPDetails {
101    /// Generates the current time-based OTP as a string, if possible.
102    ///
103    /// # Returns
104    /// - `Some(String)`: The OTP generated at the current time.
105    /// - `None`: If the TOTP client or current timestamp is unavailable.
106    #[must_use]
107    pub fn totp(&self) -> Option<String> {
108        self.totp_at(now()?)
109    }
110
111    /// Generates the current time-based OTP as a 32-bit unsigned integer, if possible.
112    ///
113    /// # Returns
114    /// - `Some(u32)`: The OTP generated at the current time as a number.
115    /// - `None`: If the TOTP client or current timestamp is unavailable.
116    #[must_use]
117    pub fn totp_u32(&self) -> Option<u32> {
118        let token = self.totp()?;
119
120        token.parse().ok()
121    }
122
123    /// Generates the next time-based OTP as a string, if possible.
124    ///
125    /// The next OTP corresponds to the token generated for the next time interval.
126    ///
127    /// # Returns
128    /// - `Some(String)`: The next OTP.
129    /// - `None`: If the TOTP client or current timestamp is unavailable.
130    #[must_use]
131    pub fn totp_next(&self) -> Option<String> {
132        let next_t = now()? + 30; // new token every 30s
133        self.totp_at(next_t)
134    }
135
136    /// Generates the next time-based OTP as a 32-bit unsigned integer, if possible.
137    ///
138    /// # Returns
139    /// - `Some(u32)`: The next OTP as a number.
140    /// - `None`: If the TOTP client or current timestamp is unavailable.
141    #[must_use]
142    pub fn totp_next_u32(&self) -> Option<u32> {
143        let token = self.totp_next()?;
144        token.parse().ok()
145    }
146
147    /// Generates the previous time-based OTP as a string, if possible.
148    ///
149    /// The previous OTP corresponds to the token generated for the last time interval.
150    ///
151    /// # Returns
152    /// - `Some(String)`: The previous OTP.
153    /// - `None`: If the TOTP client or current timestamp is unavailable.
154    #[must_use]
155    pub fn totp_previous(&self) -> Option<String> {
156        let prev_t = now()? - 30; // new token every 30s
157        self.totp_at(prev_t)
158    }
159
160    /// Generates the previous time-based OTP as a 32-bit unsigned integer, if possible.
161    ///
162    /// # Returns
163    /// - `Some(u32)`: The previous OTP as a number.
164    /// - `None`: If the TOTP client or current timestamp is unavailable.
165    #[must_use]
166    pub fn totp_previous_u32(&self) -> Option<u32> {
167        let token = self.totp_previous()?;
168        token.parse().ok()
169    }
170
171    /// Generates a time-based OTP for a specific timestamp, if possible.
172    ///
173    /// This method allows generating OTPs for custom timestamps, useful for testing or debugging.
174    ///
175    /// # Parameters
176    /// - `timestamp_sec`: The timestamp (in seconds since the Unix epoch) for which to generate the OTP.
177    ///
178    /// # Returns
179    /// - `Some(String)`: The OTP for the given timestamp.
180    /// - `None`: If the TOTP client is unavailable.
181    #[must_use]
182    pub fn totp_at(
183        &self,
184        timestamp_sec: u64,
185    ) -> Option<String> {
186        self.client
187            .as_ref()
188            .map(|client| client.generate(timestamp_sec))
189    }
190
191    /// Generates a time-based OTP for a specific timestamp as a 32-bit unsigned integer, if possible.
192    ///
193    /// # Parameters
194    /// - `timestamp_sec`: The timestamp (in seconds since the Unix epoch) for which to generate the OTP.
195    ///
196    /// # Returns
197    /// - `Some(u32)`: The OTP for the given timestamp as a number.
198    /// - `None`: If the TOTP client is unavailable.
199    #[must_use]
200    pub fn totp_at_u32(
201        &self,
202        timestamp_sec: u64,
203    ) -> Option<u32> {
204        let token = self.totp_at(timestamp_sec)?;
205        token.parse().ok()
206    }
207
208    #[expect(
209        clippy::type_complexity,
210        reason = "It's a weird type but that's okay for this internal method"
211    )]
212    const fn fields(
213        &self
214    ) -> (
215        &Option<String>,
216        &Option<String>,
217        &Option<String>,
218        &Option<String>,
219        &Option<String>,
220        &Option<i32>,
221        &Option<i32>,
222    ) {
223        // internal function to track fields used in hash, eq
224        (
225            &self.link,
226            &self.token_type,
227            &self.source,
228            &self.label,
229            &self.account,
230            &self.digits,
231            &self.period,
232        )
233    }
234}
235
236/// Represents a unique ordering of services with a positional value.
237///
238/// Typically used to define display or priority order.
239#[derive(
240    Serialize, Deserialize, Debug, Copy, Clone, PartialEq, PartialOrd, Ord, Eq, Hash, Default,
241)]
242pub struct OrderDetails {
243    position: u32,
244}
245
246/// Details of an icon collection associated with a service, identified by a unique ID.
247#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, PartialOrd, Ord, Eq, Hash, Default)]
248pub struct IconCollectionDetails {
249    id: String,
250}
251
252/// Represents icon details for a service, including the selected icon and its collection.
253#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, PartialOrd, Ord, Eq, Hash, Default)]
254pub struct IconDetails {
255    selected: String,
256    #[serde(alias = "iconCollection")]
257    icon_collection: IconCollectionDetails,
258}
259
260#[derive(Deserialize, Debug, Clone, PartialEq, PartialOrd, Ord, Eq, Hash, Default)]
261pub(crate) struct RawTwoFactorAuthDetails {
262    /// `unique_id` = hash of name+secret, calculated after deserializing
263    pub name: String,
264    secret: String,
265    #[serde(alias = "updatedAt")]
266    pub updated_at: u32,
267    #[serde(alias = "serviceTypeId")]
268    pub service_type_id: Option<String>,
269    pub otp: Option<RawOTPDetails>,
270    pub order: Option<OrderDetails>,
271    pub icon: Option<IconDetails>,
272    #[serde(alias = "groupId")]
273    pub group_id: Option<String>,
274}
275
276/// Represents a parsed and structured two-factor authentication (2FA) service.
277///
278/// This struct encapsulates all details and functionality associated with a single 2FA service, including:
279/// - Metadata such as name, updated time, and associated group.
280/// - Configuration details for generating OTPs.
281/// - Integration with a TOTP client for token generation.
282///
283/// `TwoFactorAuthDetails` provides methods to generate OTPs and query associated metadata.
284///
285/// # Example
286/// ```rust
287/// use lib2fas::load_services;
288/// use lib2fas::TwoFactorAuthDetails;
289///
290/// #[tokio::main]
291/// async fn main() {
292///     let result = load_services("path/to/services.2fas", Some("passphrase")).await;
293///     match result {
294///         Ok(storage) => {
295///             if let Some(service) = storage.first() {
296///                 // service: TwoFactorAuthDetails
297///                 println!("Service Name: {}", service.name);
298///                 if let Some(otp) = service.totp() {
299///                     println!("Current OTP: {}", otp);
300///                 } else {
301///                     println!("OTP generation not available for this service.");
302///                 }
303///             } else {
304///                 println!("No services found.");
305///             }
306///         },
307///         Err(err) => {
308///             eprintln!("Failed to load services: {}", err);
309///         },
310///     }
311/// }
312/// ```
313#[derive(Serialize, Debug, Clone, PartialEq, Eq, Default, Hash)]
314pub struct TwoFactorAuthDetails {
315    /// A unique identifier for the 2FA service, derived from its name and secret.
316    #[serde(serialize_with = "serialize_to_string")]
317    pub unique_id: u64,
318    /// The name of the 2FA service.
319    pub name: String,
320    /// The secret used for generating OTPs.
321    secret: String,
322    /// The timestamp (in seconds since the Unix epoch) when the service was last updated.
323    pub updated_at: u32,
324    /// The type of the service, if applicable.
325    pub service_type_id: Option<String>,
326    /// The OTP details associated with the service, including configuration and TOTP client.
327    pub otp: Option<OTPDetails>,
328    /// The order details of the service, used for display or prioritization.
329    pub order: Option<OrderDetails>,
330    /// The icon details of the service, including its collection and selected icon.
331    pub icon: Option<IconDetails>,
332    /// The ID of the group this service belongs to, if any.
333    pub group_id: Option<String>,
334}
335
336impl TwoFactorAuthDetails {
337    /// Generates the current time-based OTP as a string, if possible.
338    ///
339    /// # Returns
340    /// - `Some(String)`: The OTP generated at the current time.
341    /// - `None`: If OTP details or the TOTP client are unavailable.
342    #[must_use]
343    pub fn totp(&self) -> Option<String> {
344        self.otp.as_ref().and_then(OTPDetails::totp)
345    }
346
347    /// Generates the current time-based OTP as a 32-bit unsigned integer, if possible.
348    ///
349    /// # Returns
350    /// - `Some(u32)`: The OTP generated at the current time as a number.
351    /// - `None`: If OTP details or the TOTP client are unavailable.
352    #[must_use]
353    pub fn totp_u32(&self) -> Option<u32> {
354        self.otp.as_ref().and_then(OTPDetails::totp_u32)
355    }
356
357    /// Generates the next time-based OTP as a string, if possible.
358    ///
359    /// The next OTP corresponds to the token generated for the next time interval.
360    ///
361    /// # Returns
362    /// - `Some(String)`: The next OTP.
363    /// - `None`: If OTP details or the TOTP client are unavailable.
364    #[cfg(not(tarpaulin))]
365    #[must_use]
366    pub fn totp_next(&self) -> Option<String> {
367        self.otp.as_ref().and_then(OTPDetails::totp_next)
368    }
369
370    /// Generates the next time-based OTP as a 32-bit unsigned integer, if possible.
371    ///
372    /// # Returns
373    /// - `Some(u32)`: The next OTP as a number.
374    /// - `None`: If OTP details or the TOTP client are unavailable.
375    #[must_use]
376    pub fn totp_next_u32(&self) -> Option<u32> {
377        self.otp.as_ref().and_then(OTPDetails::totp_next_u32)
378    }
379
380    /// Generates the previous time-based OTP as a string, if possible.
381    ///
382    /// The previous OTP corresponds to the token generated for the last time interval.
383    ///
384    /// # Returns
385    /// - `Some(String)`: The previous OTP.
386    /// - `None`: If OTP details or the TOTP client are unavailable.
387    #[cfg(not(tarpaulin))]
388    #[must_use]
389    pub fn totp_previous(&self) -> Option<String> {
390        self.otp.as_ref().and_then(OTPDetails::totp_previous)
391    }
392
393    /// Generates the previous time-based OTP as a 32-bit unsigned integer, if possible.
394    ///
395    /// # Returns
396    /// - `Some(u32)`: The previous OTP as a number.
397    /// - `None`: If OTP details or the TOTP client are unavailable.
398    #[must_use]
399    pub fn totp_previous_u32(&self) -> Option<u32> {
400        self.otp.as_ref().and_then(OTPDetails::totp_previous_u32)
401    }
402
403    /// Generates a time-based OTP for a specific timestamp, if possible.
404    ///
405    /// This method allows generating OTPs for custom timestamps, useful for testing or debugging.
406    ///
407    /// # Parameters
408    /// - `time`: The timestamp (in seconds since the Unix epoch) for which to generate the OTP.
409    ///
410    /// # Returns
411    /// - `Some(String)`: The OTP for the given timestamp.
412    /// - `None`: If OTP details or the TOTP client are unavailable.
413    #[cfg(not(tarpaulin))]
414    #[must_use]
415    pub fn totp_at(
416        &self,
417        time: u64,
418    ) -> Option<String> {
419        self.otp.as_ref().and_then(|client| client.totp_at(time))
420    }
421
422    /// Generates a time-based OTP for a specific timestamp as a 32-bit unsigned integer, if possible.
423    ///
424    /// # Parameters
425    /// - `time`: The timestamp (in seconds since the Unix epoch) for which to generate the OTP.
426    ///
427    /// # Returns
428    /// - `Some(u32)`: The OTP for the given timestamp as a number.
429    /// - `None`: If OTP details or the TOTP client are unavailable.
430    #[must_use]
431    pub fn totp_at_u32(
432        &self,
433        time: u64,
434    ) -> Option<u32> {
435        self.otp
436            .as_ref()
437            .and_then(|client| client.totp_at_u32(time))
438    }
439}
440
441impl From<RawTwoFactorAuthDetails> for TwoFactorAuthDetails {
442    fn from(val: RawTwoFactorAuthDetails) -> Self {
443        let unique_id = calculate_hash(&format!("{}:{}", val.name, val.secret));
444        let otp_with_client = val.otp.map(Into::into);
445
446        Self {
447            unique_id,
448            name: val.name,
449            secret: val.secret,
450            updated_at: val.updated_at,
451            service_type_id: val.service_type_id,
452            otp: otp_with_client,
453            order: val.order,
454            icon: val.icon,
455            group_id: val.group_id,
456        }
457    }
458}
459
460/// Represents a group of services, including metadata and expanded state.
461#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, PartialOrd, Ord, Eq, Hash, Default)]
462pub struct Group {
463    id: String,
464    name: String,
465
466    #[serde(alias = "isExpanded")]
467    is_expanded: bool,
468
469    #[serde(alias = "updatedAt")]
470    updated_at: u32,
471}
472
473#[derive(Deserialize, Debug, Clone, PartialEq, PartialOrd, Ord, Eq, Hash, Default)]
474pub(crate) struct RawTwoFactorStorage {
475    services: Vec<RawTwoFactorAuthDetails>,
476
477    groups: Option<Vec<Group>>,
478
479    #[serde(alias = "updatedAt")]
480    updated_at: Option<u32>,
481
482    #[serde(alias = "schemaVersion")]
483    schema_version: Option<u8>, // should warn if not 4
484
485    #[serde(alias = "appVersionCode")]
486    app_version_code: Option<i32>,
487
488    #[serde(alias = "appVersionName")]
489    app_version_name: Option<String>,
490
491    #[serde(alias = "appOrigin")]
492    app_origin: Option<String>,
493
494    #[serde(alias = "servicesEncrypted")]
495    services_encrypted: Option<String>,
496}
497
498impl RawTwoFactorStorage {
499    pub fn is_encrypted(&self) -> bool {
500        self.services_encrypted
501            .as_ref()
502            .is_some_and(|value| !value.is_empty())
503    }
504
505    pub fn decrypt(
506        &mut self,
507        passphrase: &str,
508    ) -> anyhow::Result<()> {
509        if let Some(data) = &self.services_encrypted {
510            let entries = decrypt_services(data, passphrase)?;
511
512            self.services.extend(entries);
513            self.services_encrypted = None;
514        }
515
516        Ok(())
517    }
518
519    fn try_into_final(
520        mut self,
521        maybe_passphrase: Option<&str>,
522    ) -> TwofasResult {
523        if self.is_encrypted() {
524            if let Some(passphrase) = maybe_passphrase {
525                self.decrypt(passphrase)?;
526            } else {
527                return Err(TwofasError::FileEncrypted);
528            }
529        }
530
531        Ok(self.into())
532    }
533}
534
535type ServicesByName = BTreeMap<String, Vec<RawTwoFactorAuthDetails>>;
536
537#[must_use]
538fn collect_services_by_name(services: &[RawTwoFactorAuthDetails]) -> ServicesByName {
539    let mut services_by_name: ServicesByName = BTreeMap::new();
540
541    for service in services.iter().cloned() {
542        services_by_name
543            .entry(service.name.clone())
544            .or_default()
545            .push(service);
546    }
547
548    services_by_name
549}
550
551type ServicesById = BTreeMap<u64, TwoFactorAuthDetails>;
552
553#[must_use]
554fn collect_services_by_id(services: &Vec<RawTwoFactorAuthDetails>) -> ServicesById {
555    let mut service_by_id: ServicesById = BTreeMap::new();
556    for service_raw in services {
557        let service: TwoFactorAuthDetails = service_raw.clone().into();
558        service_by_id.entry(service.unique_id).or_insert(service);
559    }
560
561    service_by_id
562}
563
564impl From<RawTwoFactorStorage> for TwoFactorStorage {
565    /// assumes it's already decrypted. Otherwise, use `try_into_final`
566    fn from(val: RawTwoFactorStorage) -> Self {
567        let by_name = collect_services_by_name(&val.services);
568        let by_id = collect_services_by_id(&val.services);
569        let count = by_id.len();
570
571        Self {
572            services: by_id,
573            services_raw: by_name,
574            count,
575
576            groups: val.groups,
577            updated_at: val.updated_at,
578
579            schema_version: val.schema_version,
580            app_version_code: val.app_version_code,
581            app_version_name: val.app_version_name,
582            app_origin: val.app_origin,
583        }
584    }
585}
586
587// fn btree_keys_only<S: Serializer, K: Serialize, V>(
588//     input: &BTreeMap<K, V>,
589//     serializer: S,
590// ) -> Result<S::Ok, S::Error> {
591//     let values: Vec<&K> = input.keys().collect();
592//     values.serialize(serializer)
593// }
594
595/// Usage: `#[serde(serialize_with = "btree_values_only")]`
596fn btree_values_only<S: Serializer, K, V: Serialize>(
597    input: &BTreeMap<K, V>,
598    serializer: S,
599) -> Result<S::Ok, S::Error> {
600    let values: Vec<&V> = input.values().collect();
601    values.serialize(serializer)
602}
603
604/// Usage: `#[serde(serialize_with = "serialize_to_string")]`
605fn serialize_to_string<S: Serializer, T: ToString>(
606    input: &T,
607    serializer: S,
608) -> Result<S::Ok, S::Error> {
609    let value = input.to_string();
610    value.serialize(serializer)
611}
612
613/// Represents a storage container for two-factor authentication (2FA) services.
614///
615/// This struct provides an organized structure for managing 2FA services and associated metadata.
616/// It includes methods for querying services by ID or name, iterating over stored services, and
617/// serializing or deserializing the data.
618///
619/// # Example
620/// ```rust
621/// use lib2fas::{load_services};
622/// use lib2fas::TwoFactorStorage;
623///
624/// #[tokio::main]
625/// async fn main() {
626///     let result = load_services("path/to/services.2fas", Some("passphrase")).await;
627///     match result {
628///         Ok(storage) => {
629///             // storage: TwoFactorStorage
630///             println!("Loaded {} services", storage.len());
631///
632///             if let Some(service) = storage.first() {
633///                 println!("First service: {}", service.name);
634///             }
635///
636///             let services_named = storage.by_name("example");
637///             println!("Found {} services with the name 'example'", services_named.len());
638///         },
639///         Err(err) => {
640///             eprintln!("Failed to load services: {}", err);
641///         },
642///     }
643/// }
644/// ```
645#[derive(Serialize, Debug, Clone, PartialEq, Eq, Hash, Default)]
646pub struct TwoFactorStorage {
647    /// A map of services indexed by their unique ID.
648    #[serde(serialize_with = "btree_values_only")]
649    services: BTreeMap<u64, TwoFactorAuthDetails>,
650    /// A map of raw services indexed by their name.
651    #[serde(skip_serializing)]
652    services_raw: BTreeMap<String, Vec<RawTwoFactorAuthDetails>>,
653    /// An optional list of groups associated with the services.
654    groups: Option<Vec<Group>>,
655    /// The timestamp (in seconds since the Unix epoch) when the storage was last updated.
656    updated_at: Option<u32>,
657    /// The schema version of the stored data.
658    schema_version: Option<u8>,
659    /// The version code of the application that created the storage.
660    app_version_code: Option<i32>,
661    /// The version name of the application that created the storage.
662    app_version_name: Option<String>,
663    /// The origin or source of the application that created the storage.
664    app_origin: Option<String>,
665    /// The number of services stored.
666    count: usize,
667}
668
669impl TwoFactorStorage {
670    /// Checks whether the storage is empty.
671    ///
672    /// # Returns
673    /// - `true` if there are no services stored.
674    /// - `false` otherwise.
675    #[must_use]
676    pub fn is_empty(&self) -> bool {
677        self.services.is_empty()
678    }
679
680    /// Retrieves the first service in the storage.
681    ///
682    /// # Returns
683    /// - `Some(&TwoFactorAuthDetails)` if at least one service exists.
684    /// - `None` if the storage is empty.
685    #[must_use]
686    pub fn first(&self) -> Option<&TwoFactorAuthDetails> {
687        Some(self.services.first_key_value()?.1)
688    }
689
690    /// Retrieves a service by its unique ID.
691    ///
692    /// # Parameters
693    /// - `id`: The unique ID of the service to retrieve.
694    ///
695    /// # Returns
696    /// - `Some(&TwoFactorAuthDetails)` if the service exists.
697    /// - `None` if no service is found with the given ID.
698    #[must_use]
699    pub fn by_id(
700        &self,
701        id: u64,
702    ) -> Option<&TwoFactorAuthDetails> {
703        self.services.get(&id)
704    }
705
706    /// Retrieves all services with the specified name.
707    ///
708    /// # Parameters
709    /// - `name`: The name of the services to retrieve.
710    ///
711    /// # Returns
712    /// - A vector of `TwoFactorAuthDetails` matching the given name.
713    #[must_use]
714    pub fn by_name(
715        &self,
716        name: &str,
717    ) -> Vec<TwoFactorAuthDetails> {
718        static DEFAULT_VEC: Vec<RawTwoFactorAuthDetails> = vec![];
719        self.services_raw
720            .get(name)
721            .unwrap_or(&DEFAULT_VEC)
722            .iter()
723            .map(|it| it.clone().into())
724            .collect()
725    }
726
727    fn get(
728        &self,
729        key: &str,
730    ) -> Option<&Vec<RawTwoFactorAuthDetails>> {
731        // old, use by_id or by_name instead
732        self.services_raw.get(key)
733    }
734
735    /// Retrieves the first service with the specified name.
736    ///
737    /// # Parameters
738    /// - `name`: The name of the service to retrieve.
739    ///
740    /// # Returns
741    /// - `Some(TwoFactorAuthDetails)` if a service with the given name exists.
742    /// - `None` if no service is found with the given name.
743    #[must_use]
744    pub fn first_by_name(
745        &self,
746        name: &str,
747    ) -> Option<TwoFactorAuthDetails> {
748        self.by_name(name).first().cloned()
749    }
750
751    /// Searches for services matching the specified query string.
752    ///
753    /// This method performs a fuzzy match on the service names.
754    ///
755    /// # Parameters
756    /// - `query`: The query string to search for.
757    ///
758    /// # Returns
759    /// - A new `TwoFactorStorage` containing only the matching services.
760    #[must_use]
761    pub fn find(
762        &self,
763        query: &str,
764    ) -> Self {
765        let services_by_name: BTreeMap<String, Vec<RawTwoFactorAuthDetails>>;
766        let services_by_id: BTreeMap<u64, TwoFactorAuthDetails>;
767
768        let mut count: usize = 0;
769
770        if let Some(vec) = self.get(query) {
771            services_by_name = BTreeMap::from([(query.to_owned(), vec.to_owned())]);
772            services_by_id = collect_services_by_id(vec);
773
774            count = vec.len();
775        } else {
776            // todo: if no services was found in key, look in values? (e.g. description/notes etc)
777            let matching_services: Vec<_> = self
778                .services_raw
779                .clone()
780                .into_iter()
781                .filter_map(|(key, values)| {
782                    fuzzy_match(query, key).then(|| {
783                        count += values.len();
784                        values
785                    })
786                })
787                .flatten()
788                .collect();
789            services_by_name = collect_services_by_name(&matching_services);
790            services_by_id = collect_services_by_id(&matching_services);
791        }
792
793        Self {
794            services: services_by_id,
795            services_raw: services_by_name,
796            count,
797
798            ..self.clone()
799        }
800    }
801
802    /// Retrieves the first service matching the specified query string.
803    ///
804    /// This method performs a fuzzy match on the service names.
805    ///
806    /// # Parameters
807    /// - `query`: The query string to search for.
808    ///
809    /// # Returns
810    /// - `Some(TwoFactorAuthDetails)` if a matching service is found.
811    /// - `None` if no service matches the query.
812    #[must_use]
813    pub fn find_first(
814        &self,
815        name: &str,
816    ) -> Option<TwoFactorAuthDetails> {
817        self.find(name).first().cloned()
818    }
819
820    /// Returns the total number of services stored.
821    ///
822    /// # Returns
823    /// - The number of services in the storage.
824    #[must_use]
825    pub const fn len(&self) -> usize {
826        self.count
827    }
828
829    /// Returns an iterator over the stored services.
830    ///
831    /// # Returns
832    /// - An iterator yielding references to `TwoFactorAuthDetails`.
833    #[must_use]
834    pub fn iter(&self) -> TwoFactorIterator<&TwoFactorAuthDetails> {
835        let values: VecDeque<_> = self.services.values().collect();
836        TwoFactorIterator { values }
837    }
838
839    /// Deserializes a `TwoFactorStorage` instance from a JSON string.
840    ///
841    /// # Parameters
842    /// - `json`: The JSON string containing the serialized storage data.
843    ///
844    /// # Returns
845    /// - `Some(Self)` if deserialization is successful.
846    /// - `TwofasError` if the deserialization fails.
847    pub fn try_from_json(json: &str) -> TwofasResult {
848        let raw: RawTwoFactorStorage = json5::from_str(json)?;
849        raw.try_into_final(None)
850    }
851
852    /// Deserializes a `TwoFactorStorage` instance from a JSON string.
853    ///
854    /// # Parameters
855    /// - `json`: The JSON string containing the serialized storage data.
856    ///
857    /// # Returns
858    /// - `Some(Self)` if deserialization is successful.
859    /// - `None` if the deserialization fails.
860    #[must_use]
861    pub fn from_json(json: &str) -> Option<Self> {
862        let result: RawTwoFactorStorage = json5::from_str(json).ok()?;
863        result.try_into_final(None).ok()
864    }
865
866    /// Serializes the storage to a JSON string.
867    ///
868    /// # Returns
869    /// - `Some(String)` containing the serialized JSON representation.
870    /// - `None` if serialization fails.
871    #[must_use]
872    pub fn to_json(&self) -> Option<String> {
873        json5::to_string(self).ok()
874    }
875}
876
877/// Iterator for two-factor authentication services.
878///
879/// This struct provides an iterator for traversing the services stored in a `TwoFactorStorage` instance.
880///
881/// # Example
882/// ```rust
883/// use lib2fas::{load_services, TwoFactorStorage};
884///
885/// #[tokio::main]
886/// async fn main() {
887///     let result = load_services("path/to/services.2fas", Some("passphrase")).await;
888///     if let Ok(storage) = result {
889///         for service in storage.iter() {
890///             println!("Service Name: {}", service.name);
891///         }
892///     }
893/// }
894/// ```
895pub struct TwoFactorIterator<T> {
896    values: VecDeque<T>,
897}
898
899impl<T> Iterator for TwoFactorIterator<T> {
900    type Item = T;
901
902    fn next(&mut self) -> Option<Self::Item> {
903        self.values.pop_front()
904    }
905}
906
907impl IntoIterator for TwoFactorStorage {
908    type Item = TwoFactorAuthDetails;
909    type IntoIter = TwoFactorIterator<Self::Item>;
910
911    fn into_iter(self) -> Self::IntoIter {
912        let values: VecDeque<_> = self.services.into_values().collect();
913        Self::IntoIter { values }
914    }
915}
916
917impl<'tfs> IntoIterator for &'tfs TwoFactorStorage {
918    type Item = &'tfs TwoFactorAuthDetails;
919    type IntoIter = TwoFactorIterator<&'tfs TwoFactorAuthDetails>;
920    fn into_iter(self) -> Self::IntoIter {
921        self.iter()
922    }
923}
924
925/// Errors that can occur while using the `lib2fas` library.
926#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, PartialOrd, Ord, Eq, Hash)]
927#[non_exhaustive]
928pub enum TwofasError {
929    /// Indicates that the file is encrypted but no passphrase was provided.
930    FileEncrypted,
931    /// Represents other errors with a description of the problem.
932    Other(String),
933}
934
935#[expect(
936    clippy::min_ident_chars,
937    reason = "compatibility with `std::fmt::Display.fmt`"
938)]
939impl core::fmt::Display for TwofasError {
940    fn fmt(
941        &self,
942        f: &mut Formatter<'_>,
943    ) -> core::fmt::Result {
944        match self {
945            Self::FileEncrypted => {
946                write!(f, "File is encrypted but no passphrase was provided!")
947            },
948            Self::Other(err) => {
949                write!(f, "{err}")
950            },
951        }
952    }
953}
954
955impl From<anyhow::Error> for TwofasError {
956    fn from(value: anyhow::Error) -> Self {
957        Self::Other(value.to_string())
958    }
959}
960
961impl From<std::io::Error> for TwofasError {
962    fn from(value: std::io::Error) -> Self {
963        Self::Other(format!("file read error: {value}"))
964    }
965}
966
967impl From<json5::Error> for TwofasError {
968    fn from(value: json5::Error) -> Self {
969        Self::Other(format!("json decode error: {value}"))
970    }
971}
972
973type TwofasResult = Result<TwoFactorStorage, TwofasError>;
974
975/// Loads and parses two-factor authentication services from a given 2fas file.
976///
977/// # Overview
978/// `load_services` serves as the main entry point for the `lib2fas` library. It reads a file containing
979/// two-factor authentication service details (potentially encrypted), parses its content, and
980/// returns a structured representation of the services.
981///
982/// If the file is encrypted, a passphrase must be provided to decrypt and load the data.
983///
984/// # Parameters
985/// - `filename`: The path to the 2fas file containing the two-factor authentication services.
986///   This can be any type that implements `Into<PathBuf>`.
987/// - `passphrase`: An optional passphrase used to decrypt the file if it's encrypted. If the file
988///   is encrypted and no passphrase is provided, an error will be returned.
989///
990/// # Returns
991/// - `Ok(TwoFactorStorage)`: A structured representation of the two-factor services, including
992///   service details, groups, metadata, and additional attributes.
993/// - `Err(TwofasError)`: An error indicating the reason for failure, such as file I/O issues,
994///   parsing errors, or missing decryption passphrase for encrypted files.
995///
996/// # Errors
997/// - Returns `TwofasError::FileEncrypted` if the file is encrypted but no passphrase is provided.
998/// - Returns `TwofasError::Other` for other issues, such as file read errors or invalid content.
999///
1000/// # Example
1001/// ```rust
1002/// async fn example() {
1003///     use lib2fas::load_services;
1004///
1005///     let result = load_services("path/to/services.2fas", Some("passphrase")).await;
1006///     match result {
1007///         Ok(storage) => {
1008///             println!("Loaded {} services", storage.len());
1009///         },
1010///         Err(err) => {
1011///             eprintln!("Failed to load services: {}", err);
1012///         },
1013///     }
1014/// }
1015/// ```
1016///
1017/// # See Also
1018/// The [`TwoFactorStorage`](struct.TwoFactorStorage.html) struct contains methods for interacting
1019/// with the loaded two-factor authentication services, such as querying by name or ID, and
1020/// iterating over the stored services.
1021///
1022/// # Note
1023/// This function is asynchronous and should be awaited to complete its execution.
1024/// You can use `load_services_blocking` if you can't use async.
1025pub async fn load_services<P: Into<PathBuf>>(
1026    filename: P,
1027    passphrase: Option<&str>,
1028) -> TwofasResult {
1029    use tokio::fs::File;
1030    use tokio::io::BufReader;
1031
1032    let file = File::open(filename.into())
1033        .await
1034        .map_err(TwofasError::from)?;
1035
1036    let reader = BufReader::new(file);
1037
1038    let result: RawTwoFactorStorage = json5_from_reader_async(reader).await?;
1039
1040    result.try_into_final(passphrase)
1041}
1042
1043/// Blocking variant of `load_services`.
1044pub fn load_services_blocking<P: Into<PathBuf>>(
1045    filename: P,
1046    passphrase: Option<&str>,
1047) -> TwofasResult {
1048    use std::fs::File;
1049    use std::io::BufReader;
1050
1051    let file = File::open(filename.into()).map_err(TwofasError::from)?;
1052    let reader = BufReader::new(file);
1053
1054    let result: RawTwoFactorStorage = json5_from_reader(reader)?;
1055
1056    result.try_into_final(passphrase)
1057}