taler-rust

GNU Taler code in Rust. Largely core banking integrations.
Log | Files | Refs | Submodules | README | LICENSE

lib.rs (6911B)


      1 /*
      2   This file is part of TALER
      3   Copyright (C) 2025 Taler Systems SA
      4 
      5   TALER is free software; you can redistribute it and/or modify it under the
      6   terms of the GNU Affero General Public License as published by the Free Software
      7   Foundation; either version 3, or (at your option) any later version.
      8 
      9   TALER is distributed in the hope that it will be useful, but WITHOUT ANY
     10   WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
     11   A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more details.
     12 
     13   You should have received a copy of the GNU Affero General Public License along with
     14   TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
     15 */
     16 
     17 use std::{borrow::Cow, str::FromStr, sync::Arc};
     18 
     19 use sqlx::PgPool;
     20 use taler_api::api::{Router, TalerRouter as _};
     21 use taler_common::{
     22     config::Config,
     23     types::{
     24         iban::{Country, IBAN, IbanErrorKind, ParseIbanError},
     25         payto::{FullPayto, IbanPayto, Payto, PaytoErr, PaytoImpl, PaytoURI, TransferPayto},
     26     },
     27 };
     28 
     29 use crate::{api::MagnetApi, config::ServeCfg};
     30 
     31 pub mod api;
     32 pub mod config;
     33 pub mod constants;
     34 pub mod db;
     35 pub mod dev;
     36 pub mod magnet_api;
     37 pub mod setup;
     38 pub mod worker;
     39 
     40 pub async fn run_serve(cfg: &Config, pool: PgPool) -> anyhow::Result<()> {
     41     let cfg = ServeCfg::parse(cfg)?;
     42     let api = Arc::new(MagnetApi::start(pool, cfg.payto).await);
     43     let mut router = Router::new();
     44     if let Some(cfg) = cfg.wire_gateway {
     45         router = router.wire_gateway(api.clone(), cfg.auth.method());
     46     }
     47     if let Some(cfg) = cfg.revenue {
     48         router = router.revenue(api, cfg.auth.method());
     49     }
     50     router.serve(cfg.serve, None).await?;
     51     Ok(())
     52 }
     53 
     54 #[derive(
     55     Debug, Clone, PartialEq, Eq, serde_with::DeserializeFromStr, serde_with::SerializeDisplay,
     56 )]
     57 pub struct HuIban(IBAN);
     58 
     59 impl HuIban {
     60     #[allow(clippy::identity_op)]
     61     pub fn checksum(b: &[u8]) -> Result<(), (u8, u8)> {
     62         let expected_digit = b[7] - b'0';
     63         let sum = ((b[0] - b'0') * 9) as u16
     64             + ((b[1] - b'0') * 7) as u16
     65             + ((b[2] - b'0') * 3) as u16
     66             + ((b[3] - b'0') * 1) as u16
     67             + ((b[4] - b'0') * 9) as u16
     68             + ((b[5] - b'0') * 7) as u16
     69             + ((b[6] - b'0') * 3) as u16;
     70         let modulo = ((10 - (sum % 10)) % 10) as u8;
     71         if expected_digit != modulo {
     72             Err((expected_digit, modulo))
     73         } else {
     74             Ok(())
     75         }
     76     }
     77 
     78     fn check_bban(bban: &str) -> Result<(), HuIbanErr> {
     79         let bban = bban.as_bytes();
     80         if bban.len() != 16 && bban.len() != 24 {
     81             return Err(HuIbanErr::BbanSize(bban.len()));
     82         } else if !bban.iter().all(u8::is_ascii_digit) {
     83             return Err(HuIbanErr::Invalid);
     84         }
     85         Self::checksum(&bban[..8]).map_err(|e| HuIbanErr::checksum("bank-branch number", e))?;
     86         if bban.len() == 16 {
     87             Self::checksum(&bban[8..]).map_err(|e| HuIbanErr::checksum("account number", e))?;
     88         } else {
     89             Self::checksum(&bban[8..16])
     90                 .map_err(|e| HuIbanErr::checksum("account number first group", e))?;
     91             Self::checksum(&bban[16..])
     92                 .map_err(|e| HuIbanErr::checksum("account number second group", e))?;
     93         }
     94         Ok(())
     95     }
     96 
     97     pub fn from_bban(bban: &str) -> Result<Self, HuIbanErr> {
     98         Self::check_bban(bban)?;
     99         let full_bban = if bban.len() == 16 {
    100             Cow::Owned(format!("{bban}00000000"))
    101         } else {
    102             Cow::Borrowed(bban)
    103         };
    104         let iban = IBAN::from_parts(Country::HU, &full_bban);
    105         Ok(Self(iban))
    106     }
    107 
    108     pub fn bban(&self) -> &str {
    109         let bban = self.0.bban();
    110         bban.strip_suffix("00000000").unwrap_or(bban)
    111     }
    112 
    113     pub fn iban(&self) -> &str {
    114         self.0.as_ref()
    115     }
    116 }
    117 
    118 #[derive(Debug, thiserror::Error)]
    119 pub enum HuIbanErr {
    120     #[error("contains illegal characters (only 0-9 allowed)")]
    121     Invalid,
    122     #[error("expected an hungarian IBAN starting with HU got {0}")]
    123     Country(Country),
    124     #[error("invalid length expected 16 or 24 chars got {0}")]
    125     BbanSize(usize),
    126     #[error("invalid checksum for {0} expected {1} got {2}")]
    127     Checksum(&'static str, u8, u8),
    128     #[error(transparent)]
    129     Iban(IbanErrorKind),
    130 }
    131 
    132 impl From<ParseIbanError> for HuIbanErr {
    133     fn from(value: ParseIbanError) -> Self {
    134         Self::Iban(value.kind)
    135     }
    136 }
    137 
    138 impl HuIbanErr {
    139     fn checksum(part: &'static str, (expected, checksum): (u8, u8)) -> Self {
    140         Self::Checksum(part, expected, checksum)
    141     }
    142 }
    143 
    144 impl TryFrom<IBAN> for HuIban {
    145     type Error = HuIbanErr;
    146 
    147     fn try_from(iban: IBAN) -> Result<Self, Self::Error> {
    148         let country = iban.country();
    149         if country != Country::HU {
    150             return Err(HuIbanErr::Country(country));
    151         }
    152 
    153         Self::check_bban(iban.bban())?;
    154 
    155         Ok(Self(iban))
    156     }
    157 }
    158 
    159 impl PaytoImpl for HuIban {
    160     fn as_payto(&self) -> PaytoURI {
    161         PaytoURI::from_parts("iban", format_args!("/{}", self.0))
    162     }
    163 
    164     fn parse(raw: &PaytoURI) -> Result<Self, PaytoErr> {
    165         let iban_payto = IbanPayto::try_from(raw).map_err(PaytoErr::custom)?;
    166         HuIban::try_from(iban_payto.into_inner().iban).map_err(PaytoErr::custom)
    167     }
    168 }
    169 
    170 impl FromStr for HuIban {
    171     type Err = HuIbanErr;
    172 
    173     fn from_str(s: &str) -> Result<Self, Self::Err> {
    174         let iban: IBAN = s.parse()?;
    175         Self::try_from(iban)
    176     }
    177 }
    178 
    179 impl std::fmt::Display for HuIban {
    180     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    181         self.0.fmt(f)
    182     }
    183 }
    184 
    185 /// Parse a magnet payto URI, panic if malformed
    186 pub fn magnet_payto(url: impl AsRef<str>) -> FullHuPayto {
    187     url.as_ref().parse().expect("invalid magnet payto")
    188 }
    189 
    190 pub type HuPayto = Payto<HuIban>;
    191 pub type FullHuPayto = FullPayto<HuIban>;
    192 pub type TransferHuPayto = TransferPayto<HuIban>;
    193 
    194 #[cfg(test)]
    195 mod test {
    196     use taler_common::types::{
    197         iban::IBAN,
    198         payto::{Payto, PaytoImpl, payto},
    199     };
    200 
    201     use crate::HuIban;
    202 
    203     #[test]
    204     fn hu_iban() {
    205         for (valid, account) in [
    206             (
    207                 payto("payto://iban/HU30162000031000163100000000"),
    208                 "1620000310001631",
    209             ),
    210             (
    211                 payto("payto://iban/HU02162000031000164800000000"),
    212                 "1620000310001648",
    213             ),
    214             (
    215                 payto("payto://iban/HU60162000101006446300000000"),
    216                 "1620001010064463",
    217             ),
    218         ] {
    219             // Parsing
    220             let iban_payto: Payto<IBAN> = (&valid).try_into().unwrap();
    221             let hu_payto: HuIban = iban_payto.into_inner().try_into().unwrap();
    222             assert_eq!(hu_payto.bban(), account);
    223             // Roundtrip
    224             let iban = HuIban::from_bban(&account).unwrap();
    225             let payto = iban.as_payto();
    226             assert_eq!(payto, valid);
    227         }
    228     }
    229 }