Skip to content

Commit

Permalink
Parse phone number prefix (#1092)
Browse files Browse the repository at this point in the history
  • Loading branch information
andrei-21 authored May 29, 2024
1 parent 1a3eec2 commit 36509bd
Show file tree
Hide file tree
Showing 7 changed files with 198 additions and 7 deletions.
2 changes: 1 addition & 1 deletion parser/src/domain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ fn unicode_label(s: &str) -> IResult<&str, Label> {
}

fn label(s: &str) -> IResult<&str, Label> {
let r: IResult<&str, &str> = tag_no_case("xn--")(s);
let r: IResult<_, _> = tag_no_case("xn--")(s);
match r {
Ok((s, _tag)) => punycode_label(s),
Err(_) => unicode_label(s),
Expand Down
34 changes: 33 additions & 1 deletion parser/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
mod domain;
mod lightning_address;
mod phone_number;

use lightning_address::lightning_address;
use crate::lightning_address::lightning_address;
use crate::phone_number::phone_number;
use nom::character::complete::space0;
use nom::error::Error;
use nom::sequence::delimited;
Expand Down Expand Up @@ -36,11 +38,41 @@ pub fn parse_lightning_address(address: &str) -> Result<(), ParseError> {
}
}

pub fn parse_phone_number(number: &str) -> Result<String, ParseError> {
let r = delimited(space0, phone_number, space0)(number).finish();
match r {
Ok(("", digits)) if digits.is_empty() => Err(ParseError::Incomplete),
Ok(("", digits)) => Ok(digits),
Ok((rem, _digits)) => Err(ParseError::ExcessSuffix(number.len() - rem.len())),
Err(Error { input: "", .. }) => Err(ParseError::Incomplete),
Err(Error { input, .. }) => {
Err(ParseError::UnexpectedCharacter(number.len() - input.len()))
}
}
}

#[cfg(test)]
mod tests {
use super::parse_lightning_address as p;
use super::parse_phone_number as pn;
use super::*;

#[test]
fn test_parse_phone_number() {
assert_eq!(pn(""), Err(ParseError::Incomplete));
assert_eq!(pn(" "), Err(ParseError::Incomplete));
assert_eq!(pn(" +"), Err(ParseError::Incomplete));
assert_eq!(pn(" +1"), Ok("1".to_string()));
assert_eq!(pn(" +12"), Ok("12".to_string()));
assert_eq!(pn(" +12 "), Ok("12".to_string()));
assert_eq!(pn(" +123"), Ok("123".to_string()));
assert_eq!(pn(" +12 3"), Ok("123".to_string()));

assert_eq!(pn("+123~"), Err(ParseError::ExcessSuffix(4)));
assert_eq!(pn("+12+"), Err(ParseError::ExcessSuffix(3)));
assert_eq!(pn("+12~"), Err(ParseError::ExcessSuffix(3)));
}

#[test]
fn test_parse_lightning_address() {
assert_eq!(p(""), Err(ParseError::Incomplete));
Expand Down
17 changes: 17 additions & 0 deletions parser/src/phone_number.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
use nom::bytes::complete::take_while;
use nom::character::complete::char as nom_char;
use nom::sequence::preceded;
use nom::IResult;

fn is_digit_or_symbol(c: char) -> bool {
c.is_ascii_digit() || c.is_whitespace() || ".-/()[]".contains(c)
}

pub(crate) fn phone_number(s: &str) -> IResult<&str, String> {
let (s, digits_and_symbols) = preceded(nom_char('+'), take_while(is_digit_or_symbol))(s)?;
let digits = digits_and_symbols
.chars()
.filter(char::is_ascii_digit)
.collect::<String>();
Ok((s, digits))
}
10 changes: 10 additions & 0 deletions src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,16 @@ impl NotificationHandlingErrorCode {
}
}

#[derive(Debug, PartialEq, Eq, thiserror::Error)]
pub enum ParsePhoneNumberPrefixError {
#[error("Incomplete")]
Incomplete,
#[error("InvalidCharacter at {at}")]
InvalidCharacter { at: u32 },
#[error("UnsupportedCountry")]
UnsupportedCountry,
}

#[derive(Debug, PartialEq, Eq, thiserror::Error)]
pub enum ParsePhoneNumberError {
#[error("ParsingError")]
Expand Down
26 changes: 24 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ use crate::errors::{
pub use crate::errors::{
DecodeDataError, Error as LnError, LnUrlPayError, LnUrlPayErrorCode, LnUrlPayResult,
MnemonicError, NotificationHandlingError, NotificationHandlingErrorCode, ParsePhoneNumberError,
PayError, PayErrorCode, PayResult, Result, RuntimeErrorCode, SimpleError, UnsupportedDataType,
ParsePhoneNumberPrefixError, PayError, PayErrorCode, PayResult, Result, RuntimeErrorCode,
SimpleError, UnsupportedDataType,
};
use crate::event::LipaEventListener;
pub use crate::exchange_rate_provider::ExchangeRate;
Expand All @@ -79,6 +80,7 @@ pub use crate::payment::{
IncomingPaymentInfo, OutgoingPaymentInfo, PaymentInfo, PaymentState, Recipient,
};
pub use crate::phone_number::PhoneNumber;
use crate::phone_number::PhoneNumberPrefixParser;
pub use crate::recovery::recover_lightning_node;
pub use crate::secret::{generate_secret, mnemonic_to_secret, words_by_prefix, Secret};
pub use crate::swap::{
Expand Down Expand Up @@ -270,6 +272,7 @@ pub struct LightningNode {
analytics_interceptor: Arc<AnalyticsInterceptor>,
environment: Environment,
allowed_countries_country_iso_3166_1_alpha_2: Vec<String>,
phone_number_prefix_parser: PhoneNumberPrefixParser,
}

/// Contains the fee information for the options to resolve on-chain funds from channel closes.
Expand Down Expand Up @@ -441,6 +444,9 @@ impl LightningNode {

register_webhook_url(&rt, &sdk, &auth, &environment)?;

let phone_number_prefix_parser =
PhoneNumberPrefixParser::new(&config.phone_number_allowed_countries_iso_3166_1_alpha_2);

Ok(LightningNode {
user_preferences,
sdk,
Expand All @@ -455,6 +461,7 @@ impl LightningNode {
environment,
allowed_countries_country_iso_3166_1_alpha_2: config
.phone_number_allowed_countries_iso_3166_1_alpha_2,
phone_number_prefix_parser,
})
}

Expand Down Expand Up @@ -617,7 +624,20 @@ impl LightningNode {
))
}

/// Parse a phone number, check against the list of allowed countries (set in [`Config`]).
/// Parse a phone number prefix, check against the list of allowed countries
/// (set in [`Config::phone_number_allowed_countries_iso_3166_1_alpha_2`]).
/// The parser is not strict, it parses some invalid prefixes as valid.
///
/// Requires network: **no**
pub fn parse_phone_number_prefix(
&self,
phone_number_prefix: String,
) -> std::result::Result<(), ParsePhoneNumberPrefixError> {
self.phone_number_prefix_parser.parse(&phone_number_prefix)
}

/// Parse a phone number, check against the list of allowed countries
/// (set in [`Config::phone_number_allowed_countries_iso_3166_1_alpha_2`]).
///
/// Returns a possible lightning address, which can be checked for existence
/// with [`LightningNode::decode_data`].
Expand Down Expand Up @@ -2662,6 +2682,8 @@ impl From<parser::ParseError> for ParseError {

/// Try to parse the provided string as a lightning address, return [`ParseError`]
/// precisely indicating why parsing failed.
///
/// Requires network: **no**
pub fn parse_lightning_address(address: &str) -> std::result::Result<(), ParseError> {
parser::parse_lightning_address(address).map_err(ParseError::from)
}
Expand Down
10 changes: 10 additions & 0 deletions src/lipalightninglib.udl
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ interface LightningNode {
[Throws=DecodeDataError]
DecodedData decode_data(string data);

[Throws=ParsePhoneNumberPrefixError]
void parse_phone_number_prefix(string phone_number);

[Throws=ParsePhoneNumberError]
string parse_phone_number_to_lightning_address(string phone_number);

Expand Down Expand Up @@ -739,6 +742,13 @@ interface DecodeDataError {
Unrecognized(string msg);
};

[Error]
interface ParsePhoneNumberPrefixError {
Incomplete();
InvalidCharacter(u32 at);
UnsupportedCountry();
};

[Error]
interface ParsePhoneNumberError {
ParsingError();
Expand Down
106 changes: 103 additions & 3 deletions src/phone_number.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::errors::ParsePhoneNumberError;
use crate::errors::{ParsePhoneNumberError, ParsePhoneNumberPrefixError};
use perro::ensure;
use phonenumber::country::Id as CountryCode;
use phonenumber::metadata::DATABASE;
use phonenumber::ParseError;

#[derive(PartialEq, Debug)]
Expand All @@ -10,7 +11,7 @@ pub struct PhoneNumber {
}

impl PhoneNumber {
pub(crate) fn parse(number: &str) -> Result<PhoneNumber, ParsePhoneNumberError> {
pub(crate) fn parse(number: &str) -> Result<Self, ParsePhoneNumberError> {
let number = match phonenumber::parse(None, number) {
Ok(number) => number,
Err(ParseError::InvalidCountryCode) => {
Expand All @@ -25,7 +26,7 @@ impl PhoneNumber {
.country()
.id()
.ok_or(ParsePhoneNumberError::InvalidCountryCode)?;
Ok(PhoneNumber { e164, country_code })
Ok(Self { e164, country_code })
}

pub(crate) fn to_lightning_address(&self, domain: &str) -> String {
Expand All @@ -45,12 +46,111 @@ pub(crate) fn lightning_address_to_phone_number(address: &str, domain: &str) ->
None
}

pub(crate) struct PhoneNumberPrefixParser {
allowed_country_codes: Vec<String>,
}

impl PhoneNumberPrefixParser {
pub fn new(allowed_countries_iso_3166_1_alpha_2: &[String]) -> Self {
// Stricly speaking *ISO 3166-1 alpha-2* is not the same as *CLDR country IDs*
// and such conversion is not correct, but for the most contries such codes match.
let allowed_country_codes = allowed_countries_iso_3166_1_alpha_2
.iter()
.flat_map(|id| DATABASE.by_id(id))
.map(|m| m.country_code().to_string())
.collect::<Vec<_>>();
Self {
allowed_country_codes,
}
}

pub fn parse(&self, prefix: &str) -> Result<(), ParsePhoneNumberPrefixError> {
match parser::parse_phone_number(prefix) {
Ok(digits) => {
if self
.allowed_country_codes
.iter()
.any(|c| digits.starts_with(c))
{
Ok(())
} else if self
.allowed_country_codes
.iter()
.any(|c| c.starts_with(&digits))
{
Err(ParsePhoneNumberPrefixError::Incomplete)
} else {
Err(ParsePhoneNumberPrefixError::UnsupportedCountry)
}
}
Err(parser::ParseError::Incomplete) => Err(ParsePhoneNumberPrefixError::Incomplete),
Err(
parser::ParseError::UnexpectedCharacter(index)
| parser::ParseError::ExcessSuffix(index),
) => Err(ParsePhoneNumberPrefixError::InvalidCharacter { at: index as u32 }),
}
}
}

#[cfg(test)]
mod tests {
use super::*;

static LIPA_DOMAIN: &str = "@lipa.swiss";

#[test]
fn test_parse_phone_number_prefix() {
let ch = PhoneNumberPrefixParser::new(&["CH".to_string()]);
assert_eq!(ch.parse(""), Err(ParsePhoneNumberPrefixError::Incomplete));
assert_eq!(ch.parse("+"), Err(ParsePhoneNumberPrefixError::Incomplete));
assert_eq!(
ch.parse("+3"),
Err(ParsePhoneNumberPrefixError::UnsupportedCountry)
);
assert_eq!(ch.parse("+4"), Err(ParsePhoneNumberPrefixError::Incomplete));
assert_eq!(
ch.parse("+4 "),
Err(ParsePhoneNumberPrefixError::Incomplete)
);
assert_eq!(
ch.parse("+44"),
Err(ParsePhoneNumberPrefixError::UnsupportedCountry)
);
assert_eq!(ch.parse("+41"), Ok(()));
assert_eq!(ch.parse("+41 ("), Ok(()));
assert_eq!(ch.parse("+41 (935"), Ok(()));
assert_eq!(
ch.parse("+41a"),
Err(ParsePhoneNumberPrefixError::InvalidCharacter { at: 3 })
);

let us = PhoneNumberPrefixParser::new(&["US".to_string()]);
assert_eq!(us.parse("+"), Err(ParsePhoneNumberPrefixError::Incomplete));
assert_eq!(us.parse("+1"), Ok(()));
assert_eq!(us.parse("+12"), Ok(()));

let us_and_ch = PhoneNumberPrefixParser::new(&["US".to_string(), "CH".to_string()]);
assert_eq!(
us_and_ch.parse("+"),
Err(ParsePhoneNumberPrefixError::Incomplete)
);
assert_eq!(us_and_ch.parse("+1"), Ok(()));
assert_eq!(us_and_ch.parse("+12"), Ok(()));
assert_eq!(
us_and_ch.parse("+3"),
Err(ParsePhoneNumberPrefixError::UnsupportedCountry)
);
assert_eq!(
us_and_ch.parse("+4"),
Err(ParsePhoneNumberPrefixError::Incomplete)
);
assert_eq!(
us_and_ch.parse("+44"),
Err(ParsePhoneNumberPrefixError::UnsupportedCountry)
);
assert_eq!(us_and_ch.parse("+41"), Ok(()));
}

#[test]
fn test_parse_phone_number() {
let expected = PhoneNumber {
Expand Down

0 comments on commit 36509bd

Please sign in to comment.