payto.rs (20694B)
1 /* 2 This file is part of TALER 3 Copyright (C) 2024, 2025, 2026 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::{ 18 fmt::{Debug, Display}, 19 ops::{Deref, DerefMut}, 20 str::FromStr, 21 }; 22 23 use compact_str::CompactString; 24 use serde::{Deserialize, Serialize, de::DeserializeOwned}; 25 use serde_with::{DeserializeFromStr, SerializeDisplay}; 26 use url::Url; 27 28 use super::{ 29 amount::Amount, 30 iban::{BIC, IBAN}, 31 }; 32 use crate::types::ach::{AccountNumber, RoutingNumber}; 33 34 /// Parse a payto URI, panic if malformed 35 pub fn payto(url: impl AsRef<str>) -> PaytoURI { 36 url.as_ref().parse().expect("invalid payto") 37 } 38 39 pub trait PaytoImpl: Sized { 40 fn full(self, name: &str) -> FullPayto<Self> { 41 FullPayto::new(self, name) 42 } 43 44 fn transfer( 45 self, 46 name: &str, 47 amount: Option<Amount>, 48 subject: Option<&str>, 49 ) -> TransferPayto<Self> { 50 TransferPayto::new(self, name, amount, subject) 51 } 52 53 fn as_uri(&self) -> PaytoURI; 54 fn as_full_uri(&self, name: &str) -> PaytoURI { 55 self.as_uri().as_full_payto(name) 56 } 57 fn as_transfer_uri( 58 &self, 59 name: &str, 60 amount: Option<&Amount>, 61 subject: Option<&str>, 62 ) -> PaytoURI { 63 self.as_uri().as_transfer_payto(name, amount, subject) 64 } 65 fn parse(uri: &PaytoURI) -> Result<Self, PaytoErr>; 66 } 67 68 /// A generic RFC 8905 payto URI 69 #[derive( 70 Debug, Clone, PartialEq, Eq, serde_with::DeserializeFromStr, serde_with::SerializeDisplay, 71 )] 72 pub struct PaytoURI(Url); 73 74 impl PaytoURI { 75 pub unsafe fn from_raw(str: &str) -> Self { 76 Self(Url::from_str(str).unwrap()) 77 } 78 79 pub fn raw(&self) -> &str { 80 self.0.as_str() 81 } 82 83 pub fn from_parts(domain: &str, path: impl Display) -> Self { 84 payto(format!("payto://{domain}{path}")) 85 } 86 87 pub fn as_full_payto(self, name: &str) -> PaytoURI { 88 self.with_query([("receiver-name", name)]) 89 } 90 91 pub fn as_transfer_payto( 92 self, 93 name: &str, 94 amount: Option<&Amount>, 95 subject: Option<&str>, 96 ) -> PaytoURI { 97 self.as_full_payto(name) 98 .with_query([("amount", amount)]) 99 .with_query([("message", subject)]) 100 } 101 102 pub fn query<Q: DeserializeOwned>(&self) -> Result<Q, PaytoErr> { 103 let query = self.0.query().unwrap_or_default().as_bytes(); 104 let de = serde_urlencoded::Deserializer::new(url::form_urlencoded::parse(query)); 105 serde_path_to_error::deserialize(de).map_err(PaytoErr::Query) 106 } 107 108 fn with_query(mut self, query: impl Serialize) -> Self { 109 let mut urlencoder = self.0.query_pairs_mut(); 110 query 111 .serialize(serde_urlencoded::Serializer::new(&mut urlencoder)) 112 .unwrap(); 113 let _ = urlencoder.finish(); 114 drop(urlencoder); 115 self 116 } 117 } 118 119 impl AsRef<Url> for PaytoURI { 120 fn as_ref(&self) -> &Url { 121 &self.0 122 } 123 } 124 125 impl std::fmt::Display for PaytoURI { 126 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 127 std::fmt::Display::fmt(self.raw(), f) 128 } 129 } 130 131 #[derive(Debug, thiserror::Error)] 132 pub enum PaytoErr { 133 #[error("invalid payto URI: {0}")] 134 Url(#[from] url::ParseError), 135 #[error("malformed payto URI query: {0}")] 136 Query(#[from] serde_path_to_error::Error<serde_urlencoded::de::Error>), 137 #[error("expected a payto URI got {0}")] 138 NotPayto(CompactString), 139 #[error("unsupported payto kind, expected {0} got {1}")] 140 UnsupportedKind(&'static str, CompactString), 141 #[error("to many path segments for a {0} payto uri")] 142 TooLong(&'static str), 143 #[error("missing segment {0} in path")] 144 MissingSegment(&'static str), 145 #[error("malformed segment {0}: {1}")] 146 MalformedSegment( 147 &'static str, 148 Box<dyn std::error::Error + Sync + Send + 'static>, 149 ), 150 } 151 152 impl PaytoErr { 153 pub fn malformed_segment<E: std::error::Error + Sync + Send + 'static>( 154 segment: &'static str, 155 e: E, 156 ) -> Self { 157 Self::MalformedSegment(segment, Box::new(e)) 158 } 159 } 160 161 impl FromStr for PaytoURI { 162 type Err = PaytoErr; 163 164 fn from_str(s: &str) -> Result<Self, Self::Err> { 165 // Parse url 166 let url: Url = s.parse()?; 167 // Check scheme 168 if url.scheme() != "payto" { 169 return Err(PaytoErr::NotPayto(url.scheme().into())); 170 } 171 Ok(Self(url)) 172 } 173 } 174 175 pub type IbanPayto = Payto<BankID>; 176 pub type FullIbanPayto = FullPayto<BankID>; 177 pub type TransferIbanPayto = TransferPayto<BankID>; 178 179 #[derive(Debug, Clone, Copy, PartialEq, Eq)] 180 pub struct BankID { 181 pub iban: IBAN, 182 pub bic: Option<BIC>, 183 } 184 185 const IBAN: &str = "iban"; 186 187 impl PaytoImpl for BankID { 188 fn as_uri(&self) -> PaytoURI { 189 PaytoURI::from_parts( 190 IBAN, 191 format_args!( 192 "/{}", 193 std::fmt::from_fn(|f| { 194 if let Some(bic) = &self.bic { 195 write!(f, "{bic}/")?; 196 } 197 write!(f, "{}", self.iban) 198 }) 199 ), 200 ) 201 } 202 203 fn parse(raw: &PaytoURI) -> Result<Self, PaytoErr> { 204 let url = raw.as_ref(); 205 if url.domain() != Some(IBAN) { 206 return Err(PaytoErr::UnsupportedKind( 207 IBAN, 208 url.domain().unwrap_or_default().into(), 209 )); 210 } 211 let Some(mut segments) = url.path_segments() else { 212 return Err(PaytoErr::MissingSegment(IBAN)); 213 }; 214 let Some(first) = segments.next() else { 215 return Err(PaytoErr::MissingSegment(IBAN)); 216 }; 217 let (iban, bic) = match segments.next() { 218 Some(second) => (second, Some(first)), 219 None => (first, None), 220 }; 221 222 Ok(Self { 223 iban: iban 224 .parse() 225 .map_err(|e| PaytoErr::malformed_segment(IBAN, e))?, 226 bic: bic 227 .map(|bic| { 228 bic.parse() 229 .map_err(|e| PaytoErr::malformed_segment("bic", e)) 230 }) 231 .transpose()?, 232 }) 233 } 234 } 235 236 impl PaytoImpl for IBAN { 237 fn as_uri(&self) -> PaytoURI { 238 PaytoURI::from_parts(IBAN, format_args!("/{self}")) 239 } 240 241 fn parse(raw: &PaytoURI) -> Result<Self, PaytoErr> { 242 raw.as_ref().path_segments().unwrap_or("".split('/')); 243 let payto = BankID::parse(raw)?; 244 Ok(payto.iban) 245 } 246 } 247 248 pub type AchPayto = Payto<ACH>; 249 pub type FullAchPayto = FullPayto<ACH>; 250 pub type TransferAchPayto = TransferPayto<ACH>; 251 252 #[derive(Debug, Clone, Copy, PartialEq, Eq)] 253 pub struct ACH { 254 pub routing_number: RoutingNumber, 255 pub account_number: AccountNumber, 256 } 257 258 const ACH: &str = "ach"; 259 260 impl PaytoImpl for ACH { 261 fn as_uri(&self) -> PaytoURI { 262 PaytoURI::from_parts( 263 ACH, 264 format_args!("/{}/{}", self.routing_number, self.account_number), 265 ) 266 } 267 268 fn parse(raw: &PaytoURI) -> Result<Self, PaytoErr> { 269 let url = raw.as_ref(); 270 if url.domain() != Some(ACH) { 271 return Err(PaytoErr::UnsupportedKind( 272 ACH, 273 url.domain().unwrap_or_default().into(), 274 )); 275 } 276 let Some(mut segments) = url.path_segments() else { 277 return Err(PaytoErr::MissingSegment("routing number")); 278 }; 279 let Some(routing_number) = segments.next() else { 280 return Err(PaytoErr::MissingSegment("routing number")); 281 }; 282 let Some(account_number) = segments.next() else { 283 return Err(PaytoErr::MissingSegment("account number")); 284 }; 285 Ok(Self { 286 routing_number: routing_number 287 .parse() 288 .map_err(|e| PaytoErr::malformed_segment("routing number", e))?, 289 account_number: account_number 290 .parse() 291 .map_err(|e| PaytoErr::malformed_segment("account number", e))?, 292 }) 293 } 294 } 295 296 /// Full payto query 297 #[derive(Debug, Clone, Deserialize)] 298 pub struct FullQuery { 299 #[serde(rename = "receiver-name")] 300 receiver_name: CompactString, 301 } 302 303 /// Transfer payto query 304 #[derive(Debug, Clone, Deserialize)] 305 pub struct TransferQuery { 306 #[serde(rename = "receiver-name")] 307 receiver_name: CompactString, 308 amount: Option<Amount>, 309 message: Option<CompactString>, 310 } 311 312 /// Parsed payto query 313 #[derive(Debug, Clone, Deserialize)] 314 pub struct ParsedQuery { 315 #[serde(rename = "receiver-name")] 316 receiver_name: Option<CompactString>, 317 amount: Option<Amount>, 318 message: Option<CompactString>, 319 #[serde(rename = "ch-qrr")] 320 ch_qrr: Option<CompactString>, 321 } 322 323 #[derive(Debug, Clone, Copy, PartialEq, Eq, DeserializeFromStr, SerializeDisplay)] 324 pub struct Payto<P> { 325 inner: P, 326 } 327 328 impl<P> Payto<P> { 329 pub fn convert<T: From<P>>(self) -> Payto<T> { 330 Payto { 331 inner: self.inner.into(), 332 } 333 } 334 } 335 336 impl<P: PaytoImpl> Payto<P> { 337 pub fn new(inner: P) -> Self { 338 Self { inner } 339 } 340 341 pub fn as_uri(&self) -> PaytoURI { 342 self.inner.as_uri() 343 } 344 345 pub fn into_inner(self) -> P { 346 self.inner 347 } 348 } 349 350 impl<P: PaytoImpl> TryFrom<&PaytoURI> for Payto<P> { 351 type Error = PaytoErr; 352 353 fn try_from(value: &PaytoURI) -> Result<Self, Self::Error> { 354 Ok(Self::new(P::parse(value)?)) 355 } 356 } 357 358 impl<P: PaytoImpl> From<FullPayto<P>> for Payto<P> { 359 fn from(value: FullPayto<P>) -> Payto<P> { 360 Self::new(value.inner) 361 } 362 } 363 364 impl<P: PaytoImpl> From<TransferPayto<P>> for Payto<P> { 365 fn from(value: TransferPayto<P>) -> Payto<P> { 366 Self::new(value.inner) 367 } 368 } 369 370 impl<P: PaytoImpl> std::fmt::Display for Payto<P> { 371 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 372 std::fmt::Display::fmt(&self.as_uri(), f) 373 } 374 } 375 376 impl<P: PaytoImpl> FromStr for Payto<P> { 377 type Err = PaytoErr; 378 379 fn from_str(s: &str) -> Result<Self, Self::Err> { 380 let payto: PaytoURI = s.parse()?; 381 Self::try_from(&payto) 382 } 383 } 384 385 impl<P: PaytoImpl> Deref for Payto<P> { 386 type Target = P; 387 388 fn deref(&self) -> &Self::Target { 389 &self.inner 390 } 391 } 392 393 impl<P: PaytoImpl> DerefMut for Payto<P> { 394 fn deref_mut(&mut self) -> &mut Self::Target { 395 &mut self.inner 396 } 397 } 398 399 #[derive(Debug, Clone, PartialEq, Eq, DeserializeFromStr, SerializeDisplay)] 400 pub struct FullPayto<P> { 401 inner: P, 402 pub name: CompactString, 403 } 404 405 impl<P: PaytoImpl> FullPayto<P> { 406 pub fn new(inner: P, name: &str) -> Self { 407 Self { 408 inner, 409 name: CompactString::new(name), 410 } 411 } 412 413 pub fn as_uri(&self) -> PaytoURI { 414 self.inner.as_full_uri(&self.name) 415 } 416 417 pub fn into_inner(self) -> (P, CompactString) { 418 (self.inner, self.name) 419 } 420 } 421 422 impl<P> FullPayto<P> { 423 pub fn convert<T: From<P>>(self) -> FullPayto<T> { 424 FullPayto { 425 inner: self.inner.into(), 426 name: self.name, 427 } 428 } 429 } 430 431 impl<P: PaytoImpl> TryFrom<&PaytoURI> for FullPayto<P> { 432 type Error = PaytoErr; 433 434 fn try_from(value: &PaytoURI) -> Result<Self, Self::Error> { 435 let payto = P::parse(value)?; 436 let query: FullQuery = value.query()?; 437 Ok(Self { 438 inner: payto, 439 name: query.receiver_name, 440 }) 441 } 442 } 443 444 impl<P: PaytoImpl> From<TransferPayto<P>> for FullPayto<P> { 445 fn from(value: TransferPayto<P>) -> FullPayto<P> { 446 FullPayto { 447 inner: value.inner, 448 name: value.name, 449 } 450 } 451 } 452 453 impl<P: PaytoImpl> std::fmt::Display for FullPayto<P> { 454 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 455 std::fmt::Display::fmt(&self.as_uri(), f) 456 } 457 } 458 459 impl<P: PaytoImpl> FromStr for FullPayto<P> { 460 type Err = PaytoErr; 461 462 fn from_str(s: &str) -> Result<Self, Self::Err> { 463 let raw: PaytoURI = s.parse()?; 464 Self::try_from(&raw) 465 } 466 } 467 468 impl<P: PaytoImpl> Deref for FullPayto<P> { 469 type Target = P; 470 471 fn deref(&self) -> &Self::Target { 472 &self.inner 473 } 474 } 475 476 #[derive(Debug, Clone, PartialEq, Eq, DeserializeFromStr, SerializeDisplay)] 477 pub struct TransferPayto<P> { 478 inner: P, 479 pub name: CompactString, 480 pub amount: Option<Amount>, 481 pub subject: Option<CompactString>, 482 } 483 484 impl<P: PaytoImpl> TransferPayto<P> { 485 pub fn new(inner: P, name: &str, amount: Option<Amount>, subject: Option<&str>) -> Self { 486 Self { 487 inner, 488 name: CompactString::new(name), 489 amount, 490 subject: subject.map(CompactString::new), 491 } 492 } 493 494 pub fn as_uri(&self) -> PaytoURI { 495 self.inner 496 .as_transfer_uri(&self.name, self.amount.as_ref(), self.subject.as_deref()) 497 } 498 499 pub fn into_inner(self) -> P { 500 self.inner 501 } 502 } 503 504 impl<P: PaytoImpl> TryFrom<&PaytoURI> for TransferPayto<P> { 505 type Error = PaytoErr; 506 507 fn try_from(value: &PaytoURI) -> Result<Self, Self::Error> { 508 let payto = P::parse(value)?; 509 let query: TransferQuery = value.query()?; 510 Ok(Self { 511 inner: payto, 512 name: query.receiver_name, 513 amount: query.amount, 514 subject: query.message, 515 }) 516 } 517 } 518 519 impl<P: PaytoImpl> std::fmt::Display for TransferPayto<P> { 520 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 521 std::fmt::Display::fmt(&self.as_uri(), f) 522 } 523 } 524 525 impl<P: PaytoImpl> FromStr for TransferPayto<P> { 526 type Err = PaytoErr; 527 528 fn from_str(s: &str) -> Result<Self, Self::Err> { 529 let raw: PaytoURI = s.parse()?; 530 Self::try_from(&raw) 531 } 532 } 533 534 impl<P: PaytoImpl> Deref for TransferPayto<P> { 535 type Target = P; 536 537 fn deref(&self) -> &Self::Target { 538 &self.inner 539 } 540 } 541 542 #[derive(Debug, Clone, PartialEq, Eq, DeserializeFromStr, SerializeDisplay)] 543 pub struct ParsedPayto<P> { 544 inner: P, 545 pub name: Option<CompactString>, 546 pub amount: Option<Amount>, 547 pub subject: Option<CompactString>, 548 pub ch_qrr: Option<CompactString>, 549 } 550 551 impl<P: PaytoImpl> ParsedPayto<P> { 552 pub fn new( 553 inner: P, 554 name: Option<&str>, 555 amount: Option<Amount>, 556 subject: Option<&str>, 557 ch_qrr: Option<&str>, 558 ) -> Self { 559 Self { 560 inner, 561 name: name.map(CompactString::new), 562 amount, 563 subject: subject.map(CompactString::new), 564 ch_qrr: ch_qrr.map(CompactString::new), 565 } 566 } 567 568 pub fn as_uri(&self) -> PaytoURI { 569 self.inner 570 .as_uri() 571 .with_query([("receiver-name", &self.name)]) 572 .with_query([("amount", self.amount)]) 573 .with_query([("message", &self.subject)]) 574 .with_query([("ch-qrr", &self.ch_qrr)]) 575 } 576 577 pub fn into_inner(self) -> P { 578 self.inner 579 } 580 } 581 582 impl<P: PaytoImpl> TryFrom<&PaytoURI> for ParsedPayto<P> { 583 type Error = PaytoErr; 584 585 fn try_from(value: &PaytoURI) -> Result<Self, Self::Error> { 586 let payto = P::parse(value)?; 587 let query: ParsedQuery = value.query()?; 588 Ok(Self { 589 inner: payto, 590 name: query.receiver_name, 591 amount: query.amount, 592 subject: query.message, 593 ch_qrr: query.ch_qrr, 594 }) 595 } 596 } 597 598 impl<P: PaytoImpl> std::fmt::Display for ParsedPayto<P> { 599 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 600 std::fmt::Display::fmt(&self.as_uri(), f) 601 } 602 } 603 604 impl<P: PaytoImpl> FromStr for ParsedPayto<P> { 605 type Err = PaytoErr; 606 607 fn from_str(s: &str) -> Result<Self, Self::Err> { 608 let raw: PaytoURI = s.parse()?; 609 Self::try_from(&raw) 610 } 611 } 612 613 impl<P: PaytoImpl> Deref for ParsedPayto<P> { 614 type Target = P; 615 616 fn deref(&self) -> &Self::Target { 617 &self.inner 618 } 619 } 620 621 #[cfg(test)] 622 mod test { 623 use std::str::FromStr as _; 624 625 use crate::types::{ 626 amount::amount, 627 iban::IBAN, 628 payto::{FullPayto, ParsedPayto, Payto, TransferPayto}, 629 }; 630 631 #[test] 632 pub fn parse() { 633 let iban = IBAN::from_str("FR1420041010050500013M02606").unwrap(); 634 635 // Simple payto 636 let simple_payto = Payto::new(iban); 637 assert_eq!( 638 simple_payto, 639 Payto::from_str(&format!("payto://iban/{iban}")).unwrap() 640 ); 641 assert_eq!( 642 simple_payto, 643 Payto::try_from(&simple_payto.as_uri()).unwrap() 644 ); 645 assert_eq!( 646 simple_payto, 647 Payto::from_str(&simple_payto.as_uri().to_string()).unwrap() 648 ); 649 650 // Full payto 651 let full_payto = FullPayto::new(iban, "John Smith"); 652 assert_eq!( 653 full_payto, 654 FullPayto::from_str(&format!("payto://iban/{iban}?receiver-name=John+Smith")).unwrap() 655 ); 656 assert_eq!( 657 full_payto, 658 FullPayto::try_from(&full_payto.as_uri()).unwrap() 659 ); 660 assert_eq!( 661 full_payto, 662 FullPayto::from_str(&full_payto.as_uri().to_string()).unwrap() 663 ); 664 assert_eq!(simple_payto, full_payto.clone().into()); 665 666 // Transfer simple payto 667 let transfer_payto = TransferPayto::new(iban, "John Smith", None, None); 668 assert_eq!( 669 transfer_payto, 670 TransferPayto::from_str(&format!("payto://iban/{iban}?receiver-name=John+Smith")) 671 .unwrap() 672 ); 673 assert_eq!( 674 transfer_payto, 675 TransferPayto::try_from(&transfer_payto.as_uri()).unwrap() 676 ); 677 assert_eq!( 678 transfer_payto, 679 TransferPayto::from_str(&transfer_payto.as_uri().to_string()).unwrap() 680 ); 681 assert_eq!(full_payto, transfer_payto.clone().into()); 682 683 // Transfer full payto 684 let transfer_payto = TransferPayto::new( 685 iban, 686 "John Smith", 687 Some(amount("EUR:12")), 688 Some("Wire transfer subject"), 689 ); 690 assert_eq!( 691 transfer_payto, 692 TransferPayto::from_str(&format!("payto://iban/{iban}?receiver-name=John+Smith&amount=EUR:12&message=Wire+transfer+subject")) 693 .unwrap() 694 ); 695 assert_eq!( 696 transfer_payto, 697 TransferPayto::try_from(&transfer_payto.as_uri()).unwrap() 698 ); 699 assert_eq!( 700 transfer_payto, 701 TransferPayto::from_str(&transfer_payto.as_uri().to_string()).unwrap() 702 ); 703 assert_eq!(full_payto, transfer_payto.clone().into()); 704 705 // Parsed simple payto 706 let transfer_payto = ParsedPayto::new(iban, None, None, None, None); 707 assert_eq!( 708 transfer_payto, 709 ParsedPayto::from_str(&format!("payto://iban/{iban}")).unwrap() 710 ); 711 assert_eq!( 712 transfer_payto, 713 ParsedPayto::try_from(&transfer_payto.as_uri()).unwrap() 714 ); 715 assert_eq!( 716 transfer_payto, 717 ParsedPayto::from_str(&transfer_payto.as_uri().to_string()).unwrap() 718 ); 719 720 // Parsed full payto 721 let transfer_payto = ParsedPayto::new( 722 iban, 723 Some("John Smith"), 724 Some(amount("EUR:12")), 725 Some("Wire transfer subject"), 726 Some("REFERENCE"), 727 ); 728 assert_eq!( 729 transfer_payto, 730 ParsedPayto::from_str(&format!("payto://iban/{iban}?receiver-name=John+Smith&amount=EUR:12&message=Wire+transfer+subject&ch-qrr=REFERENCE")) 731 .unwrap() 732 ); 733 assert_eq!( 734 transfer_payto, 735 ParsedPayto::try_from(&transfer_payto.as_uri()).unwrap() 736 ); 737 assert_eq!( 738 transfer_payto, 739 ParsedPayto::from_str(&transfer_payto.as_uri().to_string()).unwrap() 740 ); 741 742 // Malformed 743 let malformed = FullPayto::<IBAN>::from_str( 744 "payto://iban/CH0400766000103138557?receiver-name=NYM%20Technologies%SA", 745 ) 746 .unwrap(); 747 assert_eq!(malformed.as_ref().to_string(), "CH0400766000103138557"); 748 assert_eq!(malformed.name, "NYM Technologies%SA"); 749 } 750 }