use std::{borrow::Cow, collections::HashSet};
use protobuf::Message;
use rustc_hash::FxHashMap;
use strum::IntoEnumIterator;
use crate::{
generated::{
metadata::METADATA,
proto::{phonemetadata::PhoneMetadataCollection, phonenumber::PhoneNumber},
},
interfaces::MatcherApi,
phonenumberutil::{
helper_types::PrefixParts,
regex_wrapper_types::{PhoneMetadataWrapper, PhoneNumberDescWrapper},
},
};
use super::{
enums::{NumberLengthType, PhoneNumberFormat, PhoneNumberType},
errors::ValidationError,
helper_constants::{
OPTIONAL_EXT_SUFFIX, PLUS_SIGN, POSSIBLE_CHARS_AFTER_EXT_LABEL,
POSSIBLE_SEPARATORS_BETWEEN_NUMBER_AND_EXT_LABEL, RFC3966_EXTN_PREFIX, RFC3966_PREFIX,
},
};
pub fn load_compiled_metadata() -> Result<PhoneMetadataCollection, protobuf::Error> {
PhoneMetadataCollection::parse_from_bytes(&METADATA)
}
pub fn get_number_desc_by_type(
metadata: &PhoneMetadataWrapper,
phone_number_type: PhoneNumberType,
) -> &PhoneNumberDescWrapper {
match phone_number_type {
PhoneNumberType::PremiumRate => &metadata.premium_rate,
PhoneNumberType::TollFree => &metadata.toll_free,
PhoneNumberType::Mobile => &metadata.mobile,
PhoneNumberType::FixedLine | PhoneNumberType::FixedLineOrMobile => &metadata.fixed_line,
PhoneNumberType::SharedCost => &metadata.shared_cost,
PhoneNumberType::VoIP => &metadata.voip,
PhoneNumberType::PersonalNumber => &metadata.personal_number,
PhoneNumberType::Pager => &metadata.pager,
PhoneNumberType::UAN => &metadata.uan,
PhoneNumberType::VoiceMail => &metadata.voicemail,
PhoneNumberType::Unknown => &metadata.general_desc,
}
}
pub fn get_number_prefix_by_format_and_calling_code(
country_calling_code: &'_ str,
number_format: PhoneNumberFormat,
) -> PrefixParts<'_> {
match number_format {
PhoneNumberFormat::E164 => {
PrefixParts::Parts2([PLUS_SIGN.into(), country_calling_code.into()])
}
PhoneNumberFormat::International => {
PrefixParts::Parts3([PLUS_SIGN.into(), country_calling_code.into(), " ".into()])
}
PhoneNumberFormat::RFC3966 => PrefixParts::Parts4([
RFC3966_PREFIX.into(),
PLUS_SIGN.into(),
country_calling_code.into(),
"-".into(),
]),
PhoneNumberFormat::National => PrefixParts::Empty,
}
}
pub fn is_national_number_suffix_of_the_other(
first_number: &PhoneNumber,
second_number: &PhoneNumber,
) -> bool {
let mut buf = itoa::Buffer::new();
let first_number_national_number = buf.format(first_number.national_number());
let mut buf = itoa::Buffer::new();
let second_number_national_number = buf.format(second_number.national_number());
first_number_national_number.ends_with(second_number_national_number)
|| second_number_national_number.ends_with(first_number_national_number)
}
pub fn extn_digits(max_length: u32) -> String {
let mut buf = itoa::Buffer::new();
let max_length_str = buf.format(max_length);
const HELPER_STR_LEN: usize = 2 + 4 + 2;
let mut expr = String::with_capacity(
HELPER_STR_LEN + super::helper_constants::DIGITS.len() + max_length_str.len(),
);
expr.push_str("([");
expr.push_str(super::helper_constants::DIGITS);
expr.push_str("]{1,");
expr.push_str(max_length_str);
expr.push_str("})");
expr
}
pub fn get_national_significant_number<'b>(
phone_number: &PhoneNumber,
buf: &'b mut zeroes_itoa::LeadingZeroBuffer,
) -> Cow<'b, str> {
buf.format(
phone_number.national_number(),
if phone_number.italian_leading_zero() {
phone_number
.number_of_leading_zeros()
.try_into()
.unwrap_or(0)
} else {
0
},
)
}
pub fn create_extn_pattern(for_parsing: bool) -> String {
const EXT_LIMIT_AFTER_EXPLICIT_LABEL: u32 = 20;
const EXT_LIMIT_AFTER_LIKELY_LABEL: u32 = 15;
const EXT_LIMIT_AFTER_AMBIGUOUS_CHAR: u32 = 9;
const EXT_LIMIT_WHEN_NOT_SURE: u32 = 6;
#[cfg(all(feature = "lite", not(feature = "regex")))]
const EXPLICIT_EXT_LABELS: &str = concat!(
"(?:",
"[eE]?[xX][tT]",
"(?:[eE][nN][sS][iI](?:[oO\u{00D3}]\u{0301}?|[\u{00F3}\u{00D3}]))?[nN]?",
"|",
"(?:[\u{FF45}\u{FF25}])?[\u{FF58}\u{FF38}][\u{FF54}\u{FF34}](?:[\u{FF4E}\u{FF2E}])?",
"|",
"[\u{0434}\u{0414}][\u{043E}\u{041E}][\u{0431}\u{0411}]",
"|",
"[aA][nN][eE][xX][oO]",
")"
);
#[cfg(all(feature = "lite", not(feature = "regex")))]
const AMBIGUOUS_EXT_LABELS: &str = concat!(
"(?:",
"[xX\u{FF58}\u{FF38}#\u{FF03}~\u{FF5E}]",
"|",
"[iI][nN][tT]",
"|",
"[\u{FF49}\u{FF29}][\u{FF4E}\u{FF2E}][\u{FF54}\u{FF34}]",
")"
);
#[cfg(feature = "regex")]
const EXPLICIT_EXT_LABELS: &str = concat!(
"(?:",
"e?xt(?:ensi(?:o\u{0301}?|\u{00F3}))?n?",
"|",
"(?:\u{FF45})?\u{FF58}\u{FF54}(?:\u{FF4E})?",
"|",
"\u{0434}\u{043E}\u{0431}",
"|",
"anexo",
")"
);
#[cfg(feature = "regex")]
const AMBIGUOUS_EXT_LABELS: &str = concat!(
"(?:",
"[x\u{FF58}#\u{FF03}~\u{FF5E}]",
"|",
"int",
"|",
"\u{FF49}\u{FF4E}\u{FF54}",
")"
);
const AMBIGUOUS_SEPARATOR: &str = "[- ]+";
let rfc_extn = fast_cat::concat_str!(
RFC3966_EXTN_PREFIX,
&extn_digits(EXT_LIMIT_AFTER_EXPLICIT_LABEL)
);
let explicit_extn = fast_cat::concat_str!(
POSSIBLE_SEPARATORS_BETWEEN_NUMBER_AND_EXT_LABEL,
EXPLICIT_EXT_LABELS,
POSSIBLE_CHARS_AFTER_EXT_LABEL,
&extn_digits(EXT_LIMIT_AFTER_EXPLICIT_LABEL),
OPTIONAL_EXT_SUFFIX
);
let ambiguous_extn = fast_cat::concat_str!(
POSSIBLE_SEPARATORS_BETWEEN_NUMBER_AND_EXT_LABEL,
AMBIGUOUS_EXT_LABELS,
POSSIBLE_CHARS_AFTER_EXT_LABEL,
&extn_digits(EXT_LIMIT_AFTER_AMBIGUOUS_CHAR),
OPTIONAL_EXT_SUFFIX
);
let american_style_extn_with_suffix = fast_cat::concat_str!(
AMBIGUOUS_SEPARATOR,
&extn_digits(EXT_LIMIT_WHEN_NOT_SURE),
"#"
);
let extension_pattern = fast_cat::concat_str!(
&rfc_extn,
"|",
&explicit_extn,
"|",
&ambiguous_extn,
"|",
&american_style_extn_with_suffix
);
if for_parsing {
let auto_dialling_and_ext_labels_found = "(?:,{2}|;)";
let possible_separators_number_ext_label_no_comma = "[ \u{00A0}\t]*";
let auto_dialling_extn = fast_cat::concat_str!(
possible_separators_number_ext_label_no_comma,
auto_dialling_and_ext_labels_found,
POSSIBLE_CHARS_AFTER_EXT_LABEL,
&extn_digits(EXT_LIMIT_AFTER_LIKELY_LABEL),
OPTIONAL_EXT_SUFFIX
);
let only_commas_extn = fast_cat::concat_str!(
possible_separators_number_ext_label_no_comma,
"(?:,)+",
POSSIBLE_CHARS_AFTER_EXT_LABEL,
&extn_digits(EXT_LIMIT_AFTER_AMBIGUOUS_CHAR),
OPTIONAL_EXT_SUFFIX
);
return fast_cat::concat_str!(
&extension_pattern,
"|",
&auto_dialling_extn,
"|",
&only_commas_extn
);
}
extension_pattern
}
pub fn normalize_helper(
normalization_replacements: &FxHashMap<char, char>,
remove_non_matches: bool,
phone_number: &str,
) -> String {
let mut normalized_number = String::with_capacity(phone_number.len());
for phone_char in phone_number.chars() {
if let Some(replacement) = normalization_replacements.get(&phone_char.to_ascii_uppercase())
{
normalized_number.push(*replacement);
} else if !remove_non_matches {
normalized_number.push(phone_char);
}
}
normalized_number
}
pub fn desc_has_possible_number_data(desc: &PhoneNumberDescWrapper) -> bool {
desc.original.possible_length.len() != 1
|| desc
.original
.possible_length
.first()
.map(|l| *l != -1)
.unwrap_or(false)
}
pub fn desc_has_data(desc: &PhoneNumberDescWrapper) -> bool {
desc.original.has_example_number()
|| desc_has_possible_number_data(desc)
|| desc.original.has_national_number_pattern()
}
pub fn populate_supported_types_for_metadata(
metadata: &PhoneMetadataWrapper,
types: &mut HashSet<PhoneNumberType>,
) {
PhoneNumberType::iter()
.filter(|number_type| {
!matches!(
number_type,
PhoneNumberType::FixedLineOrMobile | PhoneNumberType::Unknown
)
})
.filter(|number_type| desc_has_data(get_number_desc_by_type(metadata, *number_type)))
.for_each(|number_type| {
types.insert(number_type);
});
}
pub fn get_supported_types_for_metadata(
metadata: &PhoneMetadataWrapper,
) -> HashSet<PhoneNumberType> {
const EFFECTIVE_NUMBER_TYPES: usize = 11 - 2 ;
let mut types = HashSet::with_capacity(EFFECTIVE_NUMBER_TYPES);
populate_supported_types_for_metadata(metadata, &mut types);
types
}
pub fn test_number_length(
phone_number: &str,
phone_metadata: &PhoneMetadataWrapper,
phone_number_type: PhoneNumberType,
) -> Result<NumberLengthType, ValidationError> {
let desc_for_type = get_number_desc_by_type(phone_metadata, phone_number_type);
let mut possible_lengths = if desc_for_type.original.possible_length.is_empty() {
phone_metadata.general_desc.original.possible_length.clone()
} else {
desc_for_type.original.possible_length.clone()
};
let mut local_lengths = desc_for_type.original.possible_length_local_only.clone();
if phone_number_type == PhoneNumberType::FixedLineOrMobile {
let fixed_line_desc = get_number_desc_by_type(phone_metadata, PhoneNumberType::FixedLine);
if !desc_has_possible_number_data(fixed_line_desc) {
return test_number_length(phone_number, phone_metadata, PhoneNumberType::Mobile);
} else {
let mobile_desc = get_number_desc_by_type(phone_metadata, PhoneNumberType::Mobile);
if desc_has_possible_number_data(mobile_desc) {
let len_to_append = if mobile_desc.original.possible_length.is_empty() {
&phone_metadata.general_desc.original.possible_length
} else {
&mobile_desc.original.possible_length
};
possible_lengths.extend_from_slice(len_to_append);
possible_lengths.sort();
if local_lengths.is_empty() {
local_lengths = mobile_desc.original.possible_length_local_only.clone();
} else {
local_lengths
.extend_from_slice(&mobile_desc.original.possible_length_local_only);
local_lengths.sort();
}
}
}
}
if *possible_lengths.first().unwrap_or(&-1) == -1 {
return Err(ValidationError::InvalidLength);
}
let actual_length = phone_number.len() as i32;
if local_lengths.contains(&actual_length) {
return Ok(NumberLengthType::IsPossibleLocalOnly);
}
let minimum_length = possible_lengths[0];
if minimum_length == actual_length {
return Ok(NumberLengthType::IsPossible);
} else if minimum_length > actual_length {
return Err(ValidationError::TooShort);
} else if possible_lengths[possible_lengths.len() - 1] < actual_length {
return Err(ValidationError::TooLong);
}
if possible_lengths[1..].contains(&actual_length) {
Ok(NumberLengthType::IsPossible)
} else {
Err(ValidationError::InvalidLength)
}
}
pub fn test_number_length_with_unknown_type(
phone_number: &str,
phone_metadata: &PhoneMetadataWrapper,
) -> Result<NumberLengthType, ValidationError> {
test_number_length(phone_number, phone_metadata, PhoneNumberType::Unknown)
}
pub(crate) fn copy_core_fields_only(from_number: &PhoneNumber) -> PhoneNumber {
let mut to_number = PhoneNumber::new();
to_number.set_country_code(from_number.country_code());
to_number.set_national_number(from_number.national_number());
if let Some(extension) = &from_number.extension {
to_number.set_extension(extension.clone());
}
if from_number.italian_leading_zero() {
to_number.set_italian_leading_zero(true);
to_number.set_number_of_leading_zeros(from_number.number_of_leading_zeros());
}
to_number
}
pub fn is_match(
matcher_api: &dyn MatcherApi,
number: &str,
number_desc: &PhoneNumberDescWrapper,
) -> Result<bool, crate::InvalidRegexError> {
matcher_api.match_national_number(number, number_desc, false)
}