taler-rust

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

commit 425313290a4ed284a97b430a9ada4f0ac5990264
parent 2a7d903ae13ef097b4503df7394d936d3ee8265f
Author: Antoine A <>
Date:   Tue,  7 Jul 2026 18:40:13 +0200

common: fix some API found bugs

Diffstat:
MMakefile | 10+++++-----
Madapters/taler-cyclos/src/db.rs | 2+-
Madapters/taler-magnet-bank/src/db.rs | 2+-
Mcommon/taler-api/src/auth.rs | 22+++++++++++++++++-----
Mcommon/taler-api/src/db.rs | 2+-
Mcommon/taler-api/src/notification.rs | 15+++++++++------
Mcommon/taler-api/src/test/db.rs | 2+-
Mcommon/taler-common/src/api/params.rs | 12++++++++----
Mcommon/taler-common/src/api/wire.rs | 2+-
Mcommon/taler-common/src/config.rs | 8++++----
Mcommon/taler-common/src/types/amount.rs | 12++++++++----
Mcommon/taler-common/src/types/base32.rs | 2+-
Mcommon/taler-common/src/types/payto.rs | 2+-
Mcommon/taler-common/src/types/timestamp.rs | 4++--
Mcommon/taler-test-utils/Cargo.toml | 2+-
Mcommon/taler-test-utils/src/routine.rs | 2+-
16 files changed, 62 insertions(+), 39 deletions(-)

diff --git a/Makefile b/Makefile @@ -15,8 +15,8 @@ all: build build: cargo build --release --bin taler-magnet-bank --bin taler-cyclos --bin taler-apns-relay -.PHONY: install-nobuild-files -install-nobuild-files: +.PHONY: install-files +install-files: install -m 644 -D -t $(share_dir)/taler-magnet-bank/config.d adapters/taler-magnet-bank/magnet-bank.conf install -m 644 -D -t $(share_dir)/taler-magnet-bank/sql common/taler-common/db/versioning.sql install -m 644 -D -t $(share_dir)/taler-magnet-bank/sql adapters/taler-magnet-bank/db/magnet-bank*.sql @@ -37,15 +37,15 @@ install-nobuild-files: install -D -t $(bin_dir) contrib/taler-apns-relay-dbconfig .PHONY: install -install: build install-nobuild-files +install: build install-files install -D -t $(bin_dir) target/release/taler-magnet-bank install -D -t $(bin_dir) target/release/taler-cyclos install -D -t $(bin_dir) target/release/taler-apns-relay .PHONY: check -check: install-nobuild-files - cargo clippy --all-targets +check: install-files cargo test + cargo clippy --all-targets .PHONY: doc doc: diff --git a/adapters/taler-cyclos/src/db.rs b/adapters/taler-cyclos/src/db.rs @@ -684,7 +684,7 @@ pub async fn transfer_by_id( status: r.try_get(0)?, status_msg: r.try_get(1)?, amount: r.try_get_amount(2, currency)?, - origin_exchange_url: r.try_get(3)?, + exchange_base_url: r.try_get(3)?, metadata: r.try_get(4)?, wtid: r.try_get(5)?, credit_account: r.try_get_cyclos_fullpaytouri(6, 7, root)?, diff --git a/adapters/taler-magnet-bank/src/db.rs b/adapters/taler-magnet-bank/src/db.rs @@ -688,7 +688,7 @@ pub async fn transfer_by_id(db: &PgPool, id: u64) -> sqlx::Result<Option<Transfe status: r.try_get(0)?, status_msg: r.try_get(1)?, amount: r.try_get_amount(2, &CURR)?, - origin_exchange_url: r.try_get(3)?, + exchange_base_url: r.try_get(3)?, metadata: r.try_get(4)?, wtid: r.try_get(5)?, credit_account: r.try_get_iban(6)?.as_full_uri(r.try_get(7)?), diff --git a/common/taler-api/src/auth.rs b/common/taler-api/src/auth.rs @@ -80,14 +80,16 @@ pub async fn auth_middleware( return Err(failure( ErrorCode::GENERIC_UNAUTHORIZED, "Authorization header is malformed", - )); + ) + .with_header(WWW_AUTHENTICATE, challenge.clone())); }; if scheme != hscheme { return Err(failure( ErrorCode::GENERIC_UNAUTHORIZED, format!("Authorization method '{hscheme}' wrong or not supported"), - )); + ) + .with_header(WWW_AUTHENTICATE, challenge.clone())); } Ok(parameter) @@ -96,15 +98,25 @@ pub async fn auth_middleware( match &state.method { AuthMethod::Basic(token) => match parse_auth(&req, "Basic", &state.challenge) { Ok(htoken) => { - if htoken != token { - return failure_code(ErrorCode::GENERIC_TOKEN_UNKNOWN).into_response(); + if aws_lc_rs::constant_time::verify_slices_are_equal( + htoken.as_bytes(), + token.as_bytes(), + ) + .is_ok() + { + return failure_code(ErrorCode::GENERIC_UNAUTHORIZED).into_response(); } } Err(err) => return err.into_response(), }, AuthMethod::Bearer(token) => match parse_auth(&req, "Bearer", &state.challenge) { Ok(htoken) => { - if htoken != token { + if aws_lc_rs::constant_time::verify_slices_are_equal( + htoken.as_bytes(), + token.as_bytes(), + ) + .is_ok() + { return failure_code(ErrorCode::GENERIC_TOKEN_UNKNOWN).into_response(); } } diff --git a/common/taler-api/src/db.rs b/common/taler-api/src/db.rs @@ -131,7 +131,7 @@ pub async fn page<'a, 'b, R: Send + Unpin>( if params.backward() { "DESC" } else { "ASC" } )); builder - .push_bind(params.limit.abs()) + .push_bind(params.len()) .build() .try_map(map) .fetch_all(db) diff --git a/common/taler-api/src/notification.rs b/common/taler-api/src/notification.rs @@ -79,7 +79,7 @@ impl<K: Eq + Hash, V> NotificationChannel<K, V> { } } -impl<K: Eq + Hash + Clone, V: Default> NotificationChannel<K, V> { +impl<K: Eq + Hash + Clone, V: Default + Eq> NotificationChannel<K, V> { /// Subscribe to events for a specific username. /// Creates the channel lazily on first subscriber. pub fn subscribe(&self, key: K) -> watch::Receiver<V> { @@ -95,12 +95,15 @@ impl<K: Eq + Hash + Clone, V: Default> NotificationChannel<K, V> { /// Dispatch a notification to the right user's channel. pub fn dispatch(&self, key: &K, event: V) { if let Some(tx) = self.map.get(key) { - tx.send(event).ok(); - /*// send_if_modified avoids waking receivers if nothing changed + // send_if_modified avoids waking receivers if nothing changed let _ = tx.send_if_modified(|current| { - *current = Some(event.clone()); - true - });*/ + if *current != event { + *current = event; + true + } else { + false + } + }); } } diff --git a/common/taler-api/src/test/db.rs b/common/taler-api/src/test/db.rs @@ -165,7 +165,7 @@ pub async fn transfer_by_id( status: r.try_get("status")?, status_msg: r.try_get("status_msg")?, amount: r.try_get_amount("amount", currency)?, - origin_exchange_url: r.try_get("exchange_base_url")?, + exchange_base_url: r.try_get("exchange_base_url")?, metadata: r.try_get("metadata")?, wtid: r.try_get("wtid")?, credit_account: r.try_get_payto("credit_payto")?, diff --git a/common/taler-common/src/api/params.rs b/common/taler-common/src/api/params.rs @@ -43,17 +43,17 @@ pub struct PageParams { } impl PageParams { - const MAX_PAGE_SIZE: i64 = 1024; + const MAX_PAGE_SIZE: u64 = 1024; pub fn check(self) -> Result<Page, ParamsErr> { Self::check_custom(self, Self::MAX_PAGE_SIZE) } - pub fn check_custom(self, max_page_size: i64) -> Result<Page, ParamsErr> { + pub fn check_custom(self, max_page_size: u64) -> Result<Page, ParamsErr> { let limit = self.limit.unwrap_or(-20); if limit == 0 { return Err(param_err("limit", format!("must be non-zero got {limit}"))); - } else if limit > max_page_size { + } else if limit.unsigned_abs() > max_page_size { return Err(param_err( "limit", format!("must be <= {max_page_size} for {limit}"), @@ -94,6 +94,10 @@ impl Page { pub fn backward(&self) -> bool { self.limit < 0 } + + pub fn len(&self) -> i64 { + self.limit.saturating_abs() + } } #[serde_as] @@ -142,7 +146,7 @@ impl HistoryParams { pub fn check_custom( self, - max_page_size: i64, + max_page_size: u64, max_timeout_ms: u64, ) -> Result<History, ParamsErr> { Ok(History { diff --git a/common/taler-common/src/api/wire.rs b/common/taler-common/src/api/wire.rs @@ -80,7 +80,7 @@ pub struct TransferStatus { pub status: TransferState, pub status_msg: Option<String>, pub amount: Amount, - pub origin_exchange_url: String, + pub exchange_base_url: String, pub metadata: Option<CompactString>, pub wtid: ShortHashCode, pub credit_account: PaytoURI, diff --git a/common/taler-common/src/config.rs b/common/taler-common/src/config.rs @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2025, 2026 Taler Systems SA + Copyright (C) 2025-2026 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software @@ -437,7 +437,7 @@ pub mod parser { /** Search for the binary installation path in PATH */ fn install_path(&self) -> Result<PathBuf, (PathBuf, std::io::Error)> { - let path_env = std::env::var("PATH").unwrap(); + let path_env = std::env::var("PATH").unwrap_or_default(); for path_dir in path_env.split(':') { let path_dir = PathBuf::from(path_dir); let bin_path = path_dir.join(self.exec_name); @@ -893,14 +893,14 @@ impl<'cfg, 'arg> Section<'cfg, 'arg> { /** Access [option] as base32 encode bytes */ pub fn b32(&self, option: &'arg str) -> Value<'arg, Vec<u8>> { - self.value("hex", option, |it| { + self.value("b32", option, |it| { crate::encoding::base32::decode(it.as_bytes()) }) } /** Access [option] as base64 encode bytes */ pub fn b64(&self, option: &'arg str) -> Value<'arg, Vec<u8>> { - self.value("hex", option, |it| { + self.value("b64", option, |it| { crate::encoding::base64::decode(it.as_bytes()) }) } diff --git a/common/taler-common/src/types/amount.rs b/common/taler-common/src/types/amount.rs @@ -359,14 +359,18 @@ impl Amount { } pub fn try_add(self, rhs: &Self) -> Option<Self> { - assert_eq!(self.currency, rhs.currency); - let decimal = self.decimal().try_add(&rhs.decimal())?.normalize()?; + if self.currency != rhs.currency { + return None; + } + let decimal = self.decimal().try_add(&rhs.decimal())?; Some((self.currency, decimal).into()) } pub fn try_sub(self, rhs: &Self) -> Option<Self> { - assert_eq!(self.currency, rhs.currency); - let decimal = self.decimal().try_sub(&rhs.decimal())?.normalize()?; + if self.currency != rhs.currency { + return None; + } + let decimal = self.decimal().try_sub(&rhs.decimal())?; Some((self.currency, decimal).into()) } diff --git a/common/taler-common/src/types/base32.rs b/common/taler-common/src/types/base32.rs @@ -78,7 +78,7 @@ impl<const L: usize> Serialize for Base32<L> { where S: Serializer, { - serializer.serialize_str(&self.to_string()) + serializer.collect_str(&self) } } diff --git a/common/taler-common/src/types/payto.rs b/common/taler-common/src/types/payto.rs @@ -137,7 +137,7 @@ pub enum PaytoErr { NotPayto(CompactString), #[error("unsupported payto kind, expected {0} got {1}")] UnsupportedKind(&'static str, CompactString), - #[error("to much path segment for a {0} payto uri")] + #[error("to many path segments for a {0} payto uri")] TooLong(&'static str), #[error("missing segment {0} in path")] MissingSegment(&'static str), diff --git a/common/taler-common/src/types/timestamp.rs b/common/taler-common/src/types/timestamp.rs @@ -73,8 +73,8 @@ impl<'de> Deserialize<'de> for TalerTimestamp { let tmp = TimestampImpl::deserialize(deserializer)?; match tmp.t_s { Value::Number(s) => { - if let Some(since_epoch_s) = s.as_i64() { - jiff::Timestamp::from_second(since_epoch_s) + if let Some(since_epoch_s) = s.as_u64() { + jiff::Timestamp::from_second(since_epoch_s as i64) .map(Self::Timestamp) .map_err(Error::custom) } else { diff --git a/common/taler-test-utils/Cargo.toml b/common/taler-test-utils/Cargo.toml @@ -29,6 +29,6 @@ url.workspace = true aws-lc-rs.workspace = true jiff.workspace = true clap.workspace = true -reedline = "0.48" +reedline = "0.49" shlex = "2.0" nu-ansi-term = "0.50" \ No newline at end of file diff --git a/common/taler-test-utils/src/routine.rs b/common/taler-test-utils/src/routine.rs @@ -378,7 +378,7 @@ pub async fn transfer_routine( .assert_ok_json::<TransferStatus>(); assert_eq!(default_status, tx.status); assert_eq!(default_amount, tx.amount); - assert_eq!("http://exchange.taler/", tx.origin_exchange_url); + assert_eq!("http://exchange.taler/", tx.exchange_base_url); assert_eq!(req.wtid, tx.wtid); assert_eq!(first.timestamp, tx.timestamp); assert_eq!(req.metadata, tx.metadata);