Compare commits

...

7 Commits

Author SHA1 Message Date
Vlasislav Kashin
10c5ee1159 Java: added compiled metadata generation 2025-07-11 03:46:50 +03:00
Vlasislav Kashin
8a42c0ecb5 Initial copy of buildMetadata 2025-07-11 00:49:56 +03:00
Vlasislav Kashin
e75eda86e6 Add tests 2025-07-10 18:37:54 +03:00
Vlasislav Kashin
1bb46ac1b7 Fix valid phonenumber regex 2025-07-10 18:37:47 +03:00
Vlasislav Kashin
0e683eac90 Add metadata constructor for PhoneNumberUtil 2025-07-10 18:37:23 +03:00
Vlasislav Kashin
464711df8c update up to dec_from_char v0.2.0 2025-07-10 14:40:49 +03:00
Vlasislav Kashin
e52a19e6c1 Remove unused, refactor exports 2025-07-10 13:00:41 +03:00
37 changed files with 6382 additions and 312 deletions

405
Cargo.lock generated
View File

@@ -11,6 +11,56 @@ dependencies = [
"memchr",
]
[[package]]
name = "anstream"
version = "0.6.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd"
[[package]]
name = "anstyle-parse"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9"
dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys 0.59.0",
]
[[package]]
name = "anyhow"
version = "1.0.98"
@@ -35,6 +85,33 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
[[package]]
name = "colog"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c426b7af8d5e0ad79de6713996632ce31f0d68ba84068fb0d654b396e519df0"
dependencies = [
"colored",
"env_logger",
"log",
]
[[package]]
name = "colorchoice"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
[[package]]
name = "colored"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
dependencies = [
"lazy_static",
"windows-sys 0.59.0",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
@@ -78,41 +155,53 @@ dependencies = [
[[package]]
name = "dec_from_char"
version = "0.1.1"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5467a30cfe9ac2281f28ab2fc7e82b3036544bcb340f648425769fe9eb737708"
checksum = "a192e8f798f2bbd4095a4eb2f257106a1113a3b248374348809a9465419388b8"
dependencies = [
"dec_from_char_gen",
]
[[package]]
name = "dec_from_char_gen"
version = "0.1.0"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22ec31e992a6f011b864a5eda50b01cf01fa8d9d53241f4902f93d0f8024faa6"
checksum = "6ddfb9ab7ca300bfee970c933615884530ef3c97c301f17c35f09190e6729c46"
dependencies = [
"csv",
"quote",
"serde",
]
[[package]]
name = "displaydoc"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "either"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "env_filter"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0"
dependencies = [
"log",
"regex",
]
[[package]]
name = "env_logger"
version = "0.11.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f"
dependencies = [
"anstream",
"anstyle",
"env_filter",
"jiff",
"log",
]
[[package]]
name = "equivalent"
version = "1.0.2"
@@ -180,95 +269,6 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "icu_collections"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47"
dependencies = [
"displaydoc",
"potential_utf",
"yoke",
"zerofrom",
"zerovec",
]
[[package]]
name = "icu_locale_core"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a"
dependencies = [
"displaydoc",
"litemap",
"tinystr",
"writeable",
"zerovec",
]
[[package]]
name = "icu_normalizer"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979"
dependencies = [
"displaydoc",
"icu_collections",
"icu_normalizer_data",
"icu_properties",
"icu_provider",
"smallvec",
"utf16_iter",
"utf8_iter",
"write16",
"zerovec",
]
[[package]]
name = "icu_normalizer_data"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3"
[[package]]
name = "icu_properties"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b"
dependencies = [
"displaydoc",
"icu_collections",
"icu_locale_core",
"icu_properties_data",
"icu_provider",
"potential_utf",
"zerotrie",
"zerovec",
]
[[package]]
name = "icu_properties_data"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632"
[[package]]
name = "icu_provider"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af"
dependencies = [
"displaydoc",
"icu_locale_core",
"stable_deref_trait",
"tinystr",
"writeable",
"yoke",
"zerofrom",
"zerotrie",
"zerovec",
]
[[package]]
name = "indexmap"
version = "2.10.0"
@@ -279,12 +279,48 @@ dependencies = [
"hashbrown 0.15.4",
]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]]
name = "itoa"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "jiff"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49"
dependencies = [
"jiff-static",
"log",
"portable-atomic",
"portable-atomic-util",
"serde",
]
[[package]]
name = "jiff-static"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
version = "0.2.174"
@@ -303,12 +339,6 @@ version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
[[package]]
name = "litemap"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956"
[[package]]
name = "lock_api"
version = "0.4.13"
@@ -337,6 +367,12 @@ version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "once_cell_polyfill"
version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
[[package]]
name = "parking_lot_core"
version = "0.9.11"
@@ -351,12 +387,18 @@ dependencies = [
]
[[package]]
name = "potential_utf"
version = "0.1.2"
name = "portable-atomic"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585"
checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483"
[[package]]
name = "portable-atomic-util"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507"
dependencies = [
"zerovec",
"portable-atomic",
]
[[package]]
@@ -473,13 +515,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "rlibphonenumbers"
name = "rlibphonenumber"
version = "0.1.0"
dependencies = [
"colog",
"dashmap",
"dec_from_char",
"env_logger",
"fast-cat",
"icu_normalizer",
"itoa",
"log",
"protobuf",
@@ -487,7 +530,6 @@ dependencies = [
"regex",
"strum",
"thiserror 2.0.12",
"tinystr",
]
[[package]]
@@ -560,12 +602,6 @@ version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "stable_deref_trait"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]]
name = "strum"
version = "0.27.1"
@@ -599,17 +635,6 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "synstructure"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tempfile"
version = "3.20.0"
@@ -663,16 +688,6 @@ dependencies = [
"syn",
]
[[package]]
name = "tinystr"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b"
dependencies = [
"displaydoc",
"zerovec",
]
[[package]]
name = "unicode-ident"
version = "1.0.18"
@@ -680,16 +695,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[package]]
name = "utf16_iter"
version = "1.0.5"
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246"
[[package]]
name = "utf8_iter"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "wasi"
@@ -866,93 +875,3 @@ checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
dependencies = [
"bitflags",
]
[[package]]
name = "write16"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936"
[[package]]
name = "writeable"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb"
[[package]]
name = "yoke"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc"
dependencies = [
"serde",
"stable_deref_trait",
"yoke-derive",
"zerofrom",
]
[[package]]
name = "yoke-derive"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6"
dependencies = [
"proc-macro2",
"quote",
"syn",
"synstructure",
]
[[package]]
name = "zerofrom"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5"
dependencies = [
"zerofrom-derive",
]
[[package]]
name = "zerofrom-derive"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
dependencies = [
"proc-macro2",
"quote",
"syn",
"synstructure",
]
[[package]]
name = "zerotrie"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595"
dependencies = [
"displaydoc",
"yoke",
"zerofrom",
]
[[package]]
name = "zerovec"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428"
dependencies = [
"yoke",
"zerofrom",
"zerovec-derive",
]
[[package]]
name = "zerovec-derive"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f"
dependencies = [
"proc-macro2",
"quote",
"syn",
]

View File

@@ -1,5 +1,5 @@
[package]
name = "rlibphonenumbers"
name = "rlibphonenumber"
version = "0.1.0"
edition = "2024"
@@ -21,11 +21,17 @@ itoa = "1.0.15"
# simple macro for single allocation
# concatenation of strings
fast-cat = "0.1.1"
# lib for derive enum iteration
strum = { version = "0.27.1", features = ["derive"] }
icu_normalizer = "2.0.0"
tinystr = "0.8.1"
dec_from_char = "0.1.1"
# Simple lib to converts any unicode valid chars into decimals
dec_from_char = "0.2.0"
[build-dependencies]
thiserror = "2.0.12"
protobuf-codegen = "3.7.2"
[dev-dependencies]
colog = "1.3.0"
env_logger = "0.11.8"

View File

@@ -13,4 +13,23 @@ pub(crate) mod string_util;
/// boilerplate places in the code that can be replaced with macros,
/// the name of which will describe what is happening more
/// clearly than a few lines of code.
mod macros;
mod macros;
pub use phonenumberutil::{
PHONE_NUMBER_UTIL,
phonenumberutil::{
RegexResult,
MatchResult,
ParseResult,
ValidationResult,
ExampleNumberResult,
InternalLogicResult,
ExtractNumberResult,
PhoneNumberUtil
},
errors,
enums,
};
pub use proto_gen::phonemetadata;
pub use proto_gen::phonenumber;
mod tests;

View File

@@ -1,5 +1,4 @@
use strum::EnumIter;
use thiserror::Error;
/// INTERNATIONAL and NATIONAL formats are consistent with the definition
/// in ITU-T Recommendation E.123. However we follow local conventions such as

View File

@@ -1,4 +1,3 @@
use core::error;
use std::num::ParseIntError;
use thiserror::Error;

View File

@@ -1,3 +1,19 @@
/*
* Copyright (C) 2011 The Libphonenumber Authors
* Copyright (C) 2025 Vladislav Kashin (modified)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
pub const METADATA: [u8; 201802] = [
0x0A, 0xE9, 0x01, 0x0A, 0x1D, 0x12, 0x17, 0x28, 0x3F, 0x3A, 0x5B, 0x30, 0x31,
@@ -15525,4 +15541,3 @@ pub const METADATA: [u8; 201802] = [
0xFF, 0x01, 0xE2, 0x01, 0x0B, 0x48, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0x01
];

View File

@@ -0,0 +1,3 @@
pub mod metadata;
pub mod test_metadata;

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,3 @@
mod helper_constants;
mod metadata;
pub(super) use helper_constants::{*};
pub(super) use metadata::METADATA;

View File

@@ -4,18 +4,17 @@ use protobuf::Message;
use strum::IntoEnumIterator;
use crate::{
interfaces::MatcherApi,
proto_gen::{
interfaces::MatcherApi, phonenumberutil::generated::metadata::METADATA, proto_gen::{
phonemetadata::{PhoneMetadata, PhoneMetadataCollection, PhoneNumberDesc},
phonenumber::PhoneNumber,
},
}
};
use super::{
PhoneNumberFormat, PhoneNumberType, ValidNumberLenType,
errors::ValidationResultErr,
helper_constants::{
METADATA, OPTIONAL_EXT_SUFFIX, PLUS_SIGN, POSSIBLE_CHARS_AFTER_EXT_LABEL,
OPTIONAL_EXT_SUFFIX, PLUS_SIGN, POSSIBLE_CHARS_AFTER_EXT_LABEL,
POSSIBLE_SEPARATORS_BETWEEN_NUMBER_AND_EXT_LABEL, RFC3966_EXTN_PREFIX, RFC3966_PREFIX,
},
};

View File

@@ -1,19 +1,18 @@
mod helper_constants;
pub mod helper_functions;
mod errors;
mod enums;
mod phonenumberutil;
mod helper_functions;
pub mod errors;
pub mod enums;
pub mod phonenumberutil;
mod phone_number_regexps_and_mappings;
pub(self) mod helper_types;
pub(self) mod comparisons;
pub(crate) mod generated;
use std::sync::LazyLock;
pub use enums::{MatchType, PhoneNumberFormat, PhoneNumberType, ValidNumberLenType};
use thiserror::Error;
use crate::phonenumberutil::phonenumberutil::PhoneNumberUtil;
// use crate::phonenumberutil::phonenumberutil::PhoneNumberUtil;
// static PHONE_NUMBER_UTIL: LazyLock<PhoneNumberUtil> = LazyLock::new(|| {
// PhoneNumberUtil::new()
// });
pub static PHONE_NUMBER_UTIL: LazyLock<PhoneNumberUtil> = LazyLock::new(|| {
PhoneNumberUtil::new()
});

View File

@@ -268,10 +268,12 @@ impl PhoneNumberRegExpsAndMappings {
let alphanum = fast_cat::concat_str!(VALID_ALPHA_INCL_UPPERCASE, DIGITS);
let extn_patterns_for_parsing = create_extn_pattern(true);
let valid_phone_number = format!(
"{}{{{}}}|[{}]*(?:[{}{}]*{}){{3,}}[{}{}{}{}]*",
DIGITS, MIN_LENGTH_FOR_NSN, PLUS_CHARS,
// moved 2-digits pattern to an end for match full number first
"[{}]*(?:[{}{}]*{}){{3,}}[{}{}{}{}]*|{}{{{}}}",
PLUS_CHARS,
VALID_PUNCTUATION, STAR_SIGN, DIGITS,
VALID_PUNCTUATION, STAR_SIGN, VALID_ALPHA, DIGITS
VALID_PUNCTUATION, STAR_SIGN, DIGITS, VALID_ALPHA,
DIGITS, MIN_LENGTH_FOR_NSN,
);
let rfc3966_phone_digit = format!("({}|{})", DIGITS, RFC3966_VISUAL_SEPARATOR);

View File

@@ -1,10 +1,10 @@
use std::{
borrow::Cow, cmp::max, collections::{hash_map, HashMap, HashSet, VecDeque}, sync::Arc
borrow::Cow, cmp::max, collections::{HashMap, HashSet, VecDeque}, sync::Arc
};
use super::phone_number_regexps_and_mappings::PhoneNumberRegExpsAndMappings;
use crate::{
i18n, interfaces::MatcherApi, macros::owned_from_cow_or, phonenumberutil::{
i18n, interfaces::MatcherApi, macros::owned_from_cow_or, phonemetadata::PhoneMetadataCollection, phonenumberutil::{
errors::{ExtractNumberError, GetExampleNumberError, InternalLogicError, InvalidMetadataForValidRegionError, InvalidNumberError, ParseError, ValidationResultErr}, helper_constants::{
DEFAULT_EXTN_PREFIX, MAX_LENGTH_COUNTRY_CODE, MAX_LENGTH_FOR_NSN, MIN_LENGTH_FOR_NSN, NANPA_COUNTRY_CODE, PLUS_SIGN, REGION_CODE_FOR_NON_GEO_ENTITY, RFC3966_EXTN_PREFIX, RFC3966_ISDN_SUBADDRESS, RFC3966_PHONE_CONTEXT, RFC3966_PREFIX
}, helper_functions::{
@@ -59,7 +59,7 @@ pub struct PhoneNumberUtil {
}
impl PhoneNumberUtil {
pub(super) fn new() -> Self {
pub(crate) fn new_for_metadata(metadata_collection: PhoneMetadataCollection) -> Self {
let mut instance = Self {
matcher_api: Box::new(RegexBasedMatcher::new()),
reg_exps: PhoneNumberRegExpsAndMappings::new(),
@@ -68,14 +68,7 @@ impl PhoneNumberUtil {
region_to_metadata_map: Default::default(),
country_code_to_non_geographical_metadata_map: Default::default(),
};
let metadata_collection = match load_compiled_metadata() {
Err(err) => {
let err_message = format!("Could not parse compiled-in metadata: {:?}", err);
log::error!("{}", err_message);
panic!("{}", err_message);
}
Ok(metadata) => metadata,
};
// that share a country calling code when inserting data.
let mut country_calling_code_to_region_map = HashMap::<i32, VecDeque<String>>::new();
for metadata in metadata_collection.metadata {
@@ -128,7 +121,19 @@ impl PhoneNumberUtil {
instance
}
fn get_supported_regions(&self) -> impl Iterator<Item=&str> {
pub(crate) fn new() -> Self {
let metadata_collection = match load_compiled_metadata() {
Err(err) => {
let err_message = format!("Could not parse compiled-in metadata: {:?}", err);
log::error!("{}", err_message);
panic!("{}", err_message);
}
Ok(metadata) => metadata,
};
Self::new_for_metadata(metadata_collection)
}
pub(crate) fn get_supported_regions(&self) -> impl Iterator<Item=&str> {
self.region_to_metadata_map.keys().map(| k | k.as_str())
}
@@ -136,13 +141,13 @@ impl PhoneNumberUtil {
self.country_code_to_non_geographical_metadata_map.keys().map(| k | *k)
}
fn get_supported_calling_codes(&self) -> impl Iterator<Item=i32> {
pub fn get_supported_calling_codes(&self) -> impl Iterator<Item=i32> {
self.country_calling_code_to_region_code_map
.iter()
.map(| (k, _) | *k)
}
fn get_supported_types_for_region(
pub fn get_supported_types_for_region(
&self,
region_code: &str,
) -> Option<HashSet<PhoneNumberType>> {
@@ -155,7 +160,7 @@ impl PhoneNumberUtil {
})
}
fn get_supported_types_for_non_geo_entity(
pub fn get_supported_types_for_non_geo_entity(
&self,
country_calling_code: i32,
) -> Option<HashSet<PhoneNumberType>> {
@@ -975,10 +980,7 @@ impl PhoneNumberUtil {
}
pub fn normalize_digits_only<'a>(&self, phone_number: &'a str) -> String {
phone_number.chars()
.filter_map(| c | c.to_decimal_utf8())
.filter_map(| i | char::from_u32(b'0' as u32 + i) )
.collect()
dec_from_char::normalize_decimals_filtering(phone_number)
}
pub fn format_out_of_country_calling_number<'a>(
@@ -1530,7 +1532,7 @@ impl PhoneNumberUtil {
self.reg_exps.capture_up_to_second_number_start_pattern
.find(&extracted_number)
.map(move | m | m.as_str() )
.unwrap_or("")
.unwrap_or(extracted_number)
)
}
@@ -1627,7 +1629,7 @@ impl PhoneNumberUtil {
) -> ParseResult<PhoneNumber> {
let national_number = self.build_national_number_for_parsing(number_to_parse)?;
if !self.is_viable_phone_number(&national_number) {
trace!("The string supplied did not seem to be a phone number {national_number}.");
trace!("The string supplied did not seem to be a phone number '{national_number}'.");
return Err(ParseError::NotANumber)
}

View File

@@ -14,11 +14,6 @@ pub trait RegexConsume {
fn find_start<'a>(&self, s: &'a str) -> Option<Match<'a>>;
}
trait RegexMatchStart {
// Eq of looking_at
fn match_start(&self, s: &str) -> bool;
}
impl RegexFullMatch for Regex {
fn full_match(&self, s: &str) -> bool {
let found = self.find(s);
@@ -29,16 +24,6 @@ impl RegexFullMatch for Regex {
}
}
impl RegexMatchStart for Regex {
fn match_start(&self, s: &str) -> bool {
let found = self.find(s);
if let Some(matched) = found {
return matched.start() == 0;
}
false
}
}
impl RegexConsume for Regex {
fn captures_start<'a>(&self, s: &'a str) -> Option<Captures<'a>> {
let captures = self.captures(s)?;

View File

@@ -12,12 +12,7 @@ pub struct RegexCache {
}
impl RegexCache {
pub fn new() -> Self {
Self {
cache: DashMap::new(),
}
}
pub fn with_capacity(capacity: usize) -> Self {
Self {
cache: DashMap::with_capacity(capacity),

1
src/tests/mod.rs Normal file
View File

@@ -0,0 +1 @@
mod tests;

506
src/tests/tests.rs Normal file
View File

@@ -0,0 +1,506 @@
#[cfg(test)]
use std::{cell::LazyCell, sync::LazyLock};
use std::{collections::{BTreeSet, HashSet}};
use dec_from_char::DecimalExtended;
#[cfg(test)]
use env_logger::Logger;
use log::trace;
use protobuf::Message;
use crate::{errors::ParseError, phonemetadata::PhoneMetadataCollection, phonenumber::PhoneNumber, PhoneNumberUtil};
use crate::phonenumberutil::generated::test_metadata::TEST_METADATA;
// This setup function simulates getting the PhoneNumberUtil instance for each test.
fn get_phone_util() -> PhoneNumberUtil {
let metadata = PhoneMetadataCollection::parse_from_bytes(&TEST_METADATA)
.expect("Metadata should be valid");
// In a real scenario, this would likely return a singleton instance.
return PhoneNumberUtil::new_for_metadata(metadata);
}
// NOTE: To keep the translation focused on the test logic, the mock implementations
// of the methods below are omitted. The translated tests call these methods as if
// they are fully implemented in the Rust `phonenumbers` library.
// =====================================================================
// Конец секции с моками
// =====================================================================
#[test]
fn contains_only_valid_digits() {
// В оригинале это был protected-метод, но мы предполагаем, что он доступен.
fn contains_only_valid_digits(s: &str) -> bool {
// Mock implementation
!s.chars().any(|c| !c.is_decimal_utf8() && c != '')
}
assert!(contains_only_valid_digits(""));
assert!(contains_only_valid_digits("2"));
assert!(contains_only_valid_digits("25"));
assert!(contains_only_valid_digits("")); // ""
assert!(!contains_only_valid_digits("a"));
assert!(!contains_only_valid_digits("2a"));
}
#[test]
fn interchange_invalid_codepoints() {
colog::default_builder()
.filter_level(log::LevelFilter::Trace)
.init();
let phone_util = get_phone_util();
let mut phone_number = PhoneNumber::new();
let valid_inputs = vec![
"+44\u{2013}2087654321", // U+2013, EN DASH
];
for input in valid_inputs {
assert_eq!(input, dec_from_char::normalize_decimals(input));
assert!(phone_util.is_viable_phone_number(input));
phone_util.parse(input, "GB").unwrap();
}
let invalid_inputs = vec![
"+44\u{96}2087654321", // Invalid sequence
"+44\u{0096}2087654321", // U+0096
"+44\u{fffe}2087654321", // U+FFFE
];
for input in invalid_inputs {
assert!(!phone_util.is_viable_phone_number(input));
assert!(
phone_util.parse(input, "GB").is_err_and(| err | matches!(err, ParseError::NotANumber))
);
}
}
#[test]
fn get_supported_regions() {
let phone_util = get_phone_util();
assert!(phone_util
.get_supported_regions()
.count() > 0
)
}
#[test]
fn get_supported_global_network_calling_codes() {
let phone_util = get_phone_util();
let mut calling_codes = BTreeSet::<i32>::new();
// phone_util.get_supported_global_network_calling_codes(&mut calling_codes);
// assert!(!calling_codes.is_empty());
// for &code in &calling_codes {
// assert!(code > 0);
// let mut region_code = String::new();
// phone_util.get_region_code_for_country_code(code, &mut region_code);
// assert_eq!(RegionCode::un001(), region_code);
// }
}
#[test]
fn get_supported_calling_codes() {
let phone_util = get_phone_util();
let mut calling_codes = BTreeSet::<i32>::new();
// phone_util.get_supported_calling_codes(&mut calling_codes);
// assert!(!calling_codes.is_empty());
// for &code in &calling_codes {
// assert!(code > 0);
// let mut region_code = String::new();
// phone_util.get_region_code_for_country_code(code, &mut region_code);
// assert_ne!(RegionCode::zz(), region_code);
// }
// let mut supported_global_network_calling_codes = BTreeSet::<i32>::new();
// phone_util.get_supported_global_network_calling_codes(
// &mut supported_global_network_calling_codes,
// );
// assert!(calling_codes.len() > supported_global_network_calling_codes.len());
// assert!(calling_codes.contains(&979));
}
#[test]
fn get_supported_types_for_region() {
let phone_util = get_phone_util();
let mut types = HashSet::<PhoneNumber>::new();
// phone_util.get_supported_types_for_region(RegionCode::br(), &mut types);
// assert!(types.contains(&PhoneNumberType::FixedLine));
// assert!(!types.contains(&PhoneNumberType::Mobile));
// assert!(!types.contains(&PhoneNumberType::Unknown));
// types.clear();
// phone_util.get_supported_types_for_region(RegionCode::us(), &mut types);
// assert!(types.contains(&PhoneNumberType::FixedLine));
// assert!(types.contains(&PhoneNumberType::Mobile));
// assert!(!types.contains(&PhoneNumberType::FixedLineOrMobile));
// types.clear();
// phone_util.get_supported_types_for_region(RegionCode::zz(), &mut types);
// assert_eq!(0, types.len());
}
#[test]
fn get_supported_types_for_non_geo_entity() {
let phone_util = get_phone_util();
let mut types = HashSet::<PhoneNumber>::new();
// phone_util.get_supported_types_for_non_geo_entity(999, &mut types);
// assert_eq!(0, types.len());
// types.clear();
// phone_util.get_supported_types_for_non_geo_entity(979, &mut types);
// assert!(types.contains(&PhoneNumberType::PremiumRate));
// assert!(!types.contains(&PhoneNumberType::Mobile));
// assert!(!types.contains(&PhoneNumberType::Unknown));
}
#[test]
fn get_region_codes_for_country_calling_code() {
let phone_util = get_phone_util();
let mut regions = Vec::<String>::new();
// phone_util.get_region_codes_for_country_calling_code(1, &mut regions);
// assert!(regions.contains(&RegionCode::us().to_string()));
// assert!(regions.contains(&RegionCode::bs().to_string()));
// regions.clear();
// phone_util.get_region_codes_for_country_calling_code(44, &mut regions);
// assert!(regions.contains(&RegionCode::gb().to_string()));
// regions.clear();
// phone_util.get_region_codes_for_country_calling_code(49, &mut regions);
// assert!(regions.contains(&RegionCode::de().to_string()));
// regions.clear();
// phone_util.get_region_codes_for_country_calling_code(800, &mut regions);
// assert!(regions.contains(&RegionCode::un001().to_string()));
// regions.clear();
// const K_INVALID_COUNTRY_CODE: i32 = 2;
// phone_util.get_region_codes_for_country_calling_code(K_INVALID_COUNTRY_CODE, &mut regions);
// assert!(regions.is_empty());
}
#[test]
fn get_instance_load_us_metadata() {
let phone_util = get_phone_util();
// let metadata = phone_util.get_metadata_for_region(RegionCode::us()).unwrap();
// assert_eq!("US", metadata.id());
// assert_eq!(1, metadata.country_code());
// assert_eq!("011", metadata.international_prefix());
// assert!(metadata.has_national_prefix());
// assert_eq!(2, metadata.number_format().len());
// assert_eq!("(\\d{3})(\\d{3})(\\d{4})", metadata.number_format()[1].pattern());
// assert_eq!("$1 $2 $3", metadata.number_format()[1].format());
// assert_eq!("[13-689]\\d{9}|2[0-35-9]\\d{8}", metadata.general_desc().national_number_pattern());
// assert_eq!("[13-689]\\d{9}|2[0-35-9]\\d{8}", metadata.fixed_line().national_number_pattern());
// assert_eq!(1, metadata.general_desc().possible_length().len());
// assert_eq!(10, metadata.general_desc().possible_length()[0]);
// assert_eq!(0, metadata.toll_free().possible_length().len());
// assert_eq!("900\\d{7}", metadata.premium_rate().national_number_pattern());
// assert!(!metadata.shared_cost().has_national_number_pattern());
}
// ... Другие тесты, связанные с метаданными, могут быть переведены аналогично ...
#[test]
fn get_national_significant_number() {
let phone_util = get_phone_util();
let mut number = PhoneNumber::new();
number.set_country_code(1);
number.set_national_number(6502530000);
let mut national_significant_number = String::new();
// phone_util.get_national_significant_number(&number, &mut national_significant_number);
// assert_eq!("6502530000", national_significant_number);
national_significant_number.clear();
number.set_country_code(39);
number.set_national_number(312345678);
// phone_util.get_national_significant_number(&number, &mut national_significant_number);
// assert_eq!("312345678", national_significant_number);
national_significant_number.clear();
number.set_country_code(39);
number.set_national_number(236618300);
number.set_italian_leading_zero(true);
// phone_util.get_national_significant_number(&number, &mut national_significant_number);
// assert_eq!("0236618300", national_significant_number);
national_significant_number.clear();
number.clear();
number.set_country_code(800);
number.set_national_number(12345678);
// phone_util.get_national_significant_number(&number, &mut national_significant_number);
// assert_eq!("12345678", national_significant_number);
}
#[test]
fn get_national_significant_number_many_leading_zeros() {
let phone_util = get_phone_util();
let mut number = PhoneNumber::new();
number.set_country_code(1);
number.set_national_number(650);
number.set_italian_leading_zero(true);
number.set_number_of_leading_zeros(2);
let mut national_significant_number = String::new();
// phone_util.get_national_significant_number(&number, &mut national_significant_number);
// assert_eq!("00650", national_significant_number);
number.set_number_of_leading_zeros(-3);
national_significant_number.clear();
// phone_util.get_national_significant_number(&number, &mut national_significant_number);
// assert_eq!("650", national_significant_number);
}
#[test]
fn get_example_number() {
let phone_util = get_phone_util();
let mut de_number = PhoneNumber::new();
de_number.set_country_code(49);
de_number.set_national_number(30123456);
let mut test_number = PhoneNumber::new();
// let success = phone_util.get_example_number(RegionCode::de(), &mut test_number);
// assert!(success);
// assert_eq!(de_number, test_number);
// let success = phone_util.get_example_number_for_type(
// RegionCode::de(), PhoneNumberType::FixedLine, &mut test_number);
// assert!(success);
// assert_eq!(de_number, test_number);
// let success = phone_util.get_example_number_for_type(
// RegionCode::de(), PhoneNumberType::FixedLineOrMobile, &mut test_number);
// assert_eq!(de_number, test_number);
// let success = phone_util.get_example_number_for_type(
// RegionCode::de(), PhoneNumberType::Mobile, &mut test_number);
// test_number.clear();
// let success = phone_util.get_example_number_for_type(
// RegionCode::us(), PhoneNumberType::Voicemail, &mut test_number);
// assert!(!success);
// assert_eq!(PhoneNumber::new(), test_number);
// let success = phone_util.get_example_number_for_type(
// RegionCode::us(), PhoneNumberType::FixedLine, &mut test_number);
// assert!(success);
// assert_ne!(PhoneNumber::new(), test_number);
// let success = phone_util.get_example_number_for_type(
// RegionCode::us(), PhoneNumberType::Mobile, &mut test_number);
// assert!(success);
// assert_ne!(PhoneNumber::new(), test_number);
// test_number.clear();
// assert!(!phone_util.get_example_number_for_type(
// RegionCode::cs(), PhoneNumberType::Mobile, &mut test_number));
// assert_eq!(PhoneNumber::new(), test_number);
// assert!(!phone_util.get_example_number(RegionCode::un001(), &mut test_number));
}
// ... и так далее для каждого теста ...
#[test]
fn format_us_number() {
let phone_util = get_phone_util();
let mut test_number = PhoneNumber::new();
let mut formatted_number = String::new();
test_number.set_country_code(1);
test_number.set_national_number(6502530000);
// phone_util.format(&test_number, PhoneNumberFormat::National, &mut formatted_number);
// assert_eq!("650 253 0000", formatted_number);
// phone_util.format(&test_number, PhoneNumberFormat::International, &mut formatted_number);
// assert_eq!("+1 650 253 0000", formatted_number);
// ... (остальные проверки из этого теста) ...
}
#[test]
fn format_gb_number() {
let phone_util = get_phone_util();
let mut test_number = PhoneNumber::new();
let mut formatted_number = String::new();
test_number.set_country_code(44);
test_number.set_national_number(2087389353);
// phone_util.format(&test_number, PhoneNumberFormat::National, &mut formatted_number);
// assert_eq!("(020) 8738 9353", formatted_number);
// phone_util.format(&test_number, PhoneNumberFormat::International, &mut formatted_number);
// assert_eq!("+44 20 8738 9353", formatted_number);
test_number.set_national_number(7912345678);
// phone_util.format(&test_number, PhoneNumberFormat::National, &mut formatted_number);
// assert_eq!("(07912) 345 678", formatted_number);
// phone_util.format(&test_number, PhoneNumberFormat::International, &mut formatted_number);
// assert_eq!("+44 7912 345 678", formatted_number);
}
#[test]
fn is_valid_number() {
let phone_util = get_phone_util();
let mut us_number = PhoneNumber::new();
us_number.set_country_code(1);
us_number.set_national_number(6502530000);
// assert!(phone_util.is_valid_number(&us_number));
let mut it_number = PhoneNumber::new();
it_number.set_country_code(39);
it_number.set_national_number(236618300);
it_number.set_italian_leading_zero(true);
// assert!(phone_util.is_valid_number(&it_number));
// ... (остальные проверки) ...
}
#[test]
fn is_not_valid_number() {
let phone_util = get_phone_util();
let mut us_number = PhoneNumber::new();
us_number.set_country_code(1);
us_number.set_national_number(2530000);
// assert!(!phone_util.is_valid_number(&us_number));
// ... (остальные проверки) ...
}
#[test]
fn is_possible_number() {
let phone_util = get_phone_util();
let mut number = PhoneNumber::new();
number.set_country_code(1);
number.set_national_number(6502530000);
// assert!(phone_util.is_possible_number(&number));
// assert!(phone_util.is_possible_number_for_string("+1 650 253 0000", RegionCode::us()));
// assert!(phone_util.is_possible_number_for_string("253-0000", RegionCode::us()));
}
#[test]
fn is_possible_number_with_reason() {
let phone_util = get_phone_util();
let mut number = PhoneNumber::new();
number.set_country_code(1);
number.set_national_number(6502530000);
// assert_eq!(ValidationResult::IsPossible, phone_util.is_possible_number_with_reason(&number));
number.set_national_number(2530000);
// assert_eq!(ValidationResult::IsPossibleLocalOnly, phone_util.is_possible_number_with_reason(&number));
number.set_country_code(0);
// assert_eq!(ValidationResult::InvalidCountryCode, phone_util.is_possible_number_with_reason(&number));
number.set_country_code(1);
number.set_national_number(253000);
// assert_eq!(ValidationResult::TooShort, phone_util.is_possible_number_with_reason(&number));
number.set_national_number(65025300000);
// assert_eq!(ValidationResult::TooLong, phone_util.is_possible_number_with_reason(&number));
}
#[test]
fn normalise_remove_punctuation() {
let phone_util = get_phone_util();
let mut input_number = "034-56&+#2\u{ad}34".to_string();
// phone_util.normalize(&mut input_number);
let expected_output = "03456234";
// assert_eq!(expected_output, input_number, "Conversion did not correctly remove punctuation");
}
#[test]
fn normalise_replace_alpha_characters() {
let phone_util = get_phone_util();
let mut input_number = "034-I-am-HUNGRY".to_string();
// phone_util.normalize(&mut input_number);
let expected_output = "034426486479";
// assert_eq!(expected_output, input_number, "Conversion did not correctly replace alpha characters");
}
#[test]
fn maybe_strip_extension() {
let phone_util = get_phone_util();
let mut number = "1234576 ext. 1234".to_string();
let mut extension = String::new();
let expected_extension = "1234";
let stripped_number = "1234576";
// assert!(phone_util.maybe_strip_extension(&mut number, &mut extension));
// assert_eq!(stripped_number, number);
// assert_eq!(expected_extension, extension);
// ... (остальные проверки) ...
}
#[test]
fn parse_national_number() {
let phone_util = get_phone_util();
let mut nz_number = PhoneNumber::new();
nz_number.set_country_code(64);
nz_number.set_national_number(33316005);
let mut test_number = PhoneNumber::new();
// assert_eq!(ErrorType::NoParsingError, phone_util.parse("033316005", RegionCode::nz(), &mut test_number));
// assert_eq!(nz_number, test_number);
// assert!(!test_number.has_country_code_source());
// assert_eq!(CountryCodeSource::Unspecified, test_number.country_code_source());
// assert_eq!(ErrorType::NoParsingError, phone_util.parse("33316005", RegionCode::nz(), &mut test_number));
// assert_eq!(nz_number, test_number);
// ... (остальные проверки) ...
}
#[test]
fn failed_parse_on_invalid_numbers() {
let phone_util = get_phone_util();
let mut test_number = PhoneNumber::new();
// assert_eq!(ErrorType::NotANumber, phone_util.parse("This is not a phone number", RegionCode::nz(), &mut test_number));
// assert_eq!(PhoneNumber::new(), test_number);
// assert_eq!(ErrorType::TooLongNsn, phone_util.parse("01495 72553301873 810104", RegionCode::gb(), &mut test_number));
// assert_eq!(PhoneNumber::new(), test_number);
// assert_eq!(ErrorType::InvalidCountryCodeError, phone_util.parse("123 456 7890", RegionCode::get_unknown(), &mut test_number));
// assert_eq!(PhoneNumber::new(), test_number);
// ... (остальные проверки) ...
}
#[test]
fn parse_extensions() {
let phone_util = get_phone_util();
let mut nz_number = PhoneNumber::new();
nz_number.set_country_code(64);
nz_number.set_national_number(33316005);
nz_number.set_extension("3456".to_owned());
let mut test_number = PhoneNumber::new();
// assert_eq!(ErrorType::NoParsingError, phone_util.parse("03 331 6005 ext 3456", RegionCode::nz(), &mut test_number));
// assert_eq!(nz_number, test_number);
// assert_eq!(ErrorType::NoParsingError, phone_util.parse("03 331 6005 #3456", RegionCode::nz(), &mut test_number));
// assert_eq!(nz_number, test_number);
// ... (остальные проверки) ...
}
#[test]
fn can_be_internationally_dialled() {
let phone_util = get_phone_util();
let mut test_number = PhoneNumber::new();
test_number.set_country_code(1);
test_number.set_national_number(8002530000);
// assert!(!phone_util.can_be_internationally_dialled(&test_number));
test_number.set_national_number(6502530000);
// assert!(phone_util.can_be_internationally_dialled(&test_number));
// ... (остальные проверки) ...
}
#[test]
fn is_alpha_number() {
let phone_util = get_phone_util();
// assert!(phone_util.is_alpha_number("1800 six-flags"));
// assert!(phone_util.is_alpha_number("1800 six-flags ext. 1234"));
// assert!(!phone_util.is_alpha_number("1800 123-1234"));
// assert!(!phone_util.is_alpha_number("1 six-flags"));
}

3
tools/java/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
target
generated
bin

11
tools/java/Readme.md Normal file
View File

@@ -0,0 +1,11 @@
## This directory contains script for autogeneration of metadata in rust
To build from source cd /tools/java and
```
mvn install
```
Example command on build generator
```
java -jar tools\java\rust-build\target\rust-build-1.0-SNAPSHOT-jar-with-dependencies.jar BuildMetadataRustFromXml resources\PhoneNumberMetadata.xml ./test.rs metadata --const-name=test
```

78
tools/java/common/pom.xml Normal file
View File

@@ -0,0 +1,78 @@
<?xml version="1.0"?>
<project
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>tools</artifactId>
<groupId>com.google.i18n.phonenumbers</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<groupId>com.google.i18n.phonenumbers.tools</groupId>
<artifactId>common-build</artifactId>
<version>1.0-SNAPSHOT</version>
<name>Libphonenumber common library for build tools</name>
<build>
<sourceDirectory>src</sourceDirectory>
<testSourceDirectory>test</testSourceDirectory>
<testResources>
<testResource>
<directory>src/com/google/i18n/phonenumbers</directory>
<targetPath>com/google/i18n/phonenumbers</targetPath>
</testResource>
</testResources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>8</source>
<target>8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<!-- Add ../../../java/libphonenumber/src/ to make Phonemetadata.java available to the source
directories. -->
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<version>3.4.0</version>
<executions>
<execution>
<id>add-source</id>
<phase>generate-sources</phase>
<goals>
<goal>add-source</goal>
</goals>
<configuration>
<sources>
<source>../../../java/libphonenumber/src/</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/com.googlecode.libphonenumber/libphonenumber -->
<dependency>
<groupId>com.googlecode.libphonenumber</groupId>
<artifactId>libphonenumber</artifactId>
<version>9.0.9</version>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,783 @@
/*
* Copyright (C) 2009 The Libphonenumber Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.i18n.phonenumbers;
import com.google.i18n.phonenumbers.Phonemetadata.NumberFormat;
import com.google.i18n.phonenumbers.Phonemetadata.PhoneMetadata;
import com.google.i18n.phonenumbers.Phonemetadata.PhoneMetadataCollection;
import com.google.i18n.phonenumbers.Phonemetadata.PhoneNumberDesc;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
/**
* Library to build phone number metadata from the XML format.
*
* @author Shaopeng Jia
*/
public class BuildMetadataFromXml {
private static final Logger logger = Logger.getLogger(BuildMetadataFromXml.class.getName());
// String constants used to fetch the XML nodes and attributes.
private static final String CARRIER_CODE_FORMATTING_RULE = "carrierCodeFormattingRule";
private static final String CARRIER_SPECIFIC = "carrierSpecific";
private static final String COUNTRY_CODE = "countryCode";
private static final String EMERGENCY = "emergency";
private static final String EXAMPLE_NUMBER = "exampleNumber";
private static final String FIXED_LINE = "fixedLine";
private static final String FORMAT = "format";
private static final String GENERAL_DESC = "generalDesc";
private static final String INTERNATIONAL_PREFIX = "internationalPrefix";
private static final String INTL_FORMAT = "intlFormat";
private static final String LEADING_DIGITS = "leadingDigits";
private static final String MAIN_COUNTRY_FOR_CODE = "mainCountryForCode";
private static final String MOBILE = "mobile";
private static final String MOBILE_NUMBER_PORTABLE_REGION = "mobileNumberPortableRegion";
private static final String NATIONAL_NUMBER_PATTERN = "nationalNumberPattern";
private static final String NATIONAL_PREFIX = "nationalPrefix";
private static final String NATIONAL_PREFIX_FORMATTING_RULE = "nationalPrefixFormattingRule";
private static final String NATIONAL_PREFIX_OPTIONAL_WHEN_FORMATTING =
"nationalPrefixOptionalWhenFormatting";
private static final String NATIONAL_PREFIX_FOR_PARSING = "nationalPrefixForParsing";
private static final String NATIONAL_PREFIX_TRANSFORM_RULE = "nationalPrefixTransformRule";
private static final String NO_INTERNATIONAL_DIALLING = "noInternationalDialling";
private static final String NUMBER_FORMAT = "numberFormat";
private static final String PAGER = "pager";
private static final String PATTERN = "pattern";
private static final String PERSONAL_NUMBER = "personalNumber";
private static final String POSSIBLE_LENGTHS = "possibleLengths";
private static final String NATIONAL = "national";
private static final String LOCAL_ONLY = "localOnly";
private static final String PREFERRED_EXTN_PREFIX = "preferredExtnPrefix";
private static final String PREFERRED_INTERNATIONAL_PREFIX = "preferredInternationalPrefix";
private static final String PREMIUM_RATE = "premiumRate";
private static final String SHARED_COST = "sharedCost";
private static final String SHORT_CODE = "shortCode";
private static final String SMS_SERVICES = "smsServices";
private static final String STANDARD_RATE = "standardRate";
private static final String TOLL_FREE = "tollFree";
private static final String UAN = "uan";
private static final String VOICEMAIL = "voicemail";
private static final String VOIP = "voip";
private static final Set<String> PHONE_NUMBER_DESCS_WITHOUT_MATCHING_TYPES =
new HashSet<String>(Arrays.asList(new String[]{NO_INTERNATIONAL_DIALLING}));
// Build the PhoneMetadataCollection from the input XML file.
public static PhoneMetadataCollection buildPhoneMetadataCollection(String inputXmlFile,
boolean liteBuild, boolean specialBuild) throws Exception {
DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = builderFactory.newDocumentBuilder();
File xmlFile = new File(inputXmlFile);
Document document = builder.parse(xmlFile);
// TODO: Look for other uses of these constants and possibly pull them out into a separate
// constants file.
boolean isShortNumberMetadata = inputXmlFile.contains("ShortNumberMetadata");
boolean isAlternateFormatsMetadata = inputXmlFile.contains("PhoneNumberAlternateFormats");
return buildPhoneMetadataCollection(document, liteBuild, specialBuild,
isShortNumberMetadata, isAlternateFormatsMetadata);
}
// @VisibleForTesting
static PhoneMetadataCollection buildPhoneMetadataCollection(Document document,
boolean liteBuild, boolean specialBuild, boolean isShortNumberMetadata,
boolean isAlternateFormatsMetadata) throws Exception {
document.getDocumentElement().normalize();
Element rootElement = document.getDocumentElement();
NodeList territory = rootElement.getElementsByTagName("territory");
PhoneMetadataCollection.Builder metadataCollection = PhoneMetadataCollection.newBuilder();
int numOfTerritories = territory.getLength();
// TODO: Infer filter from a single flag.
MetadataFilter metadataFilter = getMetadataFilter(liteBuild, specialBuild);
for (int i = 0; i < numOfTerritories; i++) {
Element territoryElement = (Element) territory.item(i);
String regionCode = "";
// For the main metadata file this should always be set, but for other supplementary data
// files the country calling code may be all that is needed.
if (territoryElement.hasAttribute("id")) {
regionCode = territoryElement.getAttribute("id");
}
PhoneMetadata.Builder metadata = loadCountryMetadata(regionCode, territoryElement,
isShortNumberMetadata, isAlternateFormatsMetadata);
metadataFilter.filterMetadata(metadata);
metadataCollection.addMetadata(metadata.build());
}
return metadataCollection.build();
}
// Build a mapping from a country calling code to the region codes which denote the country/region
// represented by that country code. In the case of multiple countries sharing a calling code,
// such as the NANPA countries, the one indicated with "isMainCountryForCode" in the metadata
// should be first.
public static Map<Integer, List<String>> buildCountryCodeToRegionCodeMap(
PhoneMetadataCollection metadataCollection) {
Map<Integer, List<String>> countryCodeToRegionCodeMap = new TreeMap<Integer, List<String>>();
for (PhoneMetadata metadata : metadataCollection.getMetadataList()) {
String regionCode = metadata.getId();
int countryCode = metadata.getCountryCode();
if (countryCodeToRegionCodeMap.containsKey(countryCode)) {
if (metadata.getMainCountryForCode()) {
countryCodeToRegionCodeMap.get(countryCode).add(0, regionCode);
} else {
countryCodeToRegionCodeMap.get(countryCode).add(regionCode);
}
} else {
// For most countries, there will be only one region code for the country calling code.
List<String> listWithRegionCode = new ArrayList<String>(1);
if (!regionCode.equals("")) { // For alternate formats, there are no region codes at all.
listWithRegionCode.add(regionCode);
}
countryCodeToRegionCodeMap.put(countryCode, listWithRegionCode);
}
}
return countryCodeToRegionCodeMap;
}
// Build a list of region codes from the metadata
public static List<String> buildRegionCodeList(
PhoneMetadataCollection metadataCollection) {
List<String> regionCodeList = new ArrayList<String>();
for (PhoneMetadata metadata : metadataCollection.getMetadataList()) {
String regionCode = metadata.getId();
regionCodeList.add(regionCode);
}
return regionCodeList;
}
private static String validateRE(String regex) {
return validateRE(regex, false);
}
// @VisibleForTesting
static String validateRE(String regex, boolean removeWhitespace) {
// Removes all the whitespace and newline from the regexp. Not using pattern compile options to
// make it work across programming languages.
String compressedRegex = removeWhitespace ? regex.replaceAll("\\s", "") : regex;
Pattern.compile(compressedRegex);
// We don't ever expect to see | followed by a ) in our metadata - this would be an indication
// of a bug. If one wants to make something optional, we prefer ? to using an empty group.
int errorIndex = compressedRegex.indexOf("|)");
if (errorIndex >= 0) {
logger.log(Level.SEVERE, "Error with original regex: " + regex
+ "\n| should not be followed directly by ) in phone number regular expressions.");
throw new PatternSyntaxException("| followed by )", compressedRegex, errorIndex);
}
// return the regex if it is of correct syntax, i.e. compile did not fail with a
// PatternSyntaxException.
return compressedRegex;
}
/**
* Returns the national prefix of the provided country element.
*/
// @VisibleForTesting
static String getNationalPrefix(Element element) {
return element.hasAttribute(NATIONAL_PREFIX) ? element.getAttribute(NATIONAL_PREFIX) : "";
}
// @VisibleForTesting
static PhoneMetadata.Builder loadTerritoryTagMetadata(String regionCode, Element element,
String nationalPrefix) {
PhoneMetadata.Builder metadata = PhoneMetadata.newBuilder();
metadata.setId(regionCode);
if (element.hasAttribute(COUNTRY_CODE)) {
metadata.setCountryCode(Integer.parseInt(element.getAttribute(COUNTRY_CODE)));
}
if (element.hasAttribute(LEADING_DIGITS)) {
metadata.setLeadingDigits(validateRE(element.getAttribute(LEADING_DIGITS)));
}
if (element.hasAttribute(INTERNATIONAL_PREFIX)) {
metadata.setInternationalPrefix(validateRE(element.getAttribute(INTERNATIONAL_PREFIX)));
}
if (element.hasAttribute(PREFERRED_INTERNATIONAL_PREFIX)) {
metadata.setPreferredInternationalPrefix(
element.getAttribute(PREFERRED_INTERNATIONAL_PREFIX));
}
if (element.hasAttribute(NATIONAL_PREFIX_FOR_PARSING)) {
metadata.setNationalPrefixForParsing(
validateRE(element.getAttribute(NATIONAL_PREFIX_FOR_PARSING), true));
if (element.hasAttribute(NATIONAL_PREFIX_TRANSFORM_RULE)) {
metadata.setNationalPrefixTransformRule(
validateRE(element.getAttribute(NATIONAL_PREFIX_TRANSFORM_RULE)));
}
}
if (!nationalPrefix.isEmpty()) {
metadata.setNationalPrefix(nationalPrefix);
if (!metadata.hasNationalPrefixForParsing()) {
metadata.setNationalPrefixForParsing(nationalPrefix);
}
}
if (element.hasAttribute(PREFERRED_EXTN_PREFIX)) {
metadata.setPreferredExtnPrefix(element.getAttribute(PREFERRED_EXTN_PREFIX));
}
if (element.hasAttribute(MAIN_COUNTRY_FOR_CODE)) {
metadata.setMainCountryForCode(true);
}
if (element.hasAttribute(MOBILE_NUMBER_PORTABLE_REGION)) {
metadata.setMobileNumberPortableRegion(true);
}
return metadata;
}
/**
* Extracts the pattern for international format. If there is no intlFormat, default to using the
* national format. If the intlFormat is set to "NA" the intlFormat should be ignored.
*
* @throws RuntimeException if multiple intlFormats have been encountered.
* @return whether an international number format is defined.
*/
// @VisibleForTesting
static boolean loadInternationalFormat(PhoneMetadata.Builder metadata,
Element numberFormatElement,
NumberFormat nationalFormat) {
NumberFormat.Builder intlFormat = NumberFormat.newBuilder();
NodeList intlFormatPattern = numberFormatElement.getElementsByTagName(INTL_FORMAT);
boolean hasExplicitIntlFormatDefined = false;
if (intlFormatPattern.getLength() > 1) {
logger.log(Level.SEVERE,
"A maximum of one intlFormat pattern for a numberFormat element should be defined.");
String countryId = metadata.getId().length() > 0 ? metadata.getId()
: Integer.toString(metadata.getCountryCode());
throw new RuntimeException("Invalid number of intlFormat patterns for country: " + countryId);
} else if (intlFormatPattern.getLength() == 0) {
// Default to use the same as the national pattern if none is defined.
intlFormat.mergeFrom(nationalFormat);
} else {
intlFormat.setPattern(numberFormatElement.getAttribute(PATTERN));
setLeadingDigitsPatterns(numberFormatElement, intlFormat);
String intlFormatPatternValue = intlFormatPattern.item(0).getFirstChild().getNodeValue();
if (!intlFormatPatternValue.equals("NA")) {
intlFormat.setFormat(intlFormatPatternValue);
}
hasExplicitIntlFormatDefined = true;
}
if (intlFormat.hasFormat()) {
metadata.addIntlNumberFormat(intlFormat.build());
}
return hasExplicitIntlFormatDefined;
}
/**
* Extracts the pattern for the national format.
*
* @throws RuntimeException if multiple or no formats have been encountered.
*/
// @VisibleForTesting
static void loadNationalFormat(PhoneMetadata.Builder metadata, Element numberFormatElement,
NumberFormat.Builder format) {
setLeadingDigitsPatterns(numberFormatElement, format);
format.setPattern(validateRE(numberFormatElement.getAttribute(PATTERN)));
NodeList formatPattern = numberFormatElement.getElementsByTagName(FORMAT);
int numFormatPatterns = formatPattern.getLength();
if (numFormatPatterns != 1) {
logger.log(Level.SEVERE, "One format pattern for a numberFormat element should be defined.");
String countryId = metadata.getId().length() > 0 ? metadata.getId()
: Integer.toString(metadata.getCountryCode());
throw new RuntimeException("Invalid number of format patterns (" + numFormatPatterns
+ ") for country: " + countryId);
}
format.setFormat(formatPattern.item(0).getFirstChild().getNodeValue());
}
/**
* Extracts the available formats from the provided DOM element. If it does not contain any
* nationalPrefixFormattingRule, the one passed-in is retained; similarly for
* nationalPrefixOptionalWhenFormatting. The nationalPrefix, nationalPrefixFormattingRule and
* nationalPrefixOptionalWhenFormatting values are provided from the parent (territory) element.
*/
// @VisibleForTesting
static void loadAvailableFormats(PhoneMetadata.Builder metadata,
Element element, String nationalPrefix,
String nationalPrefixFormattingRule,
boolean nationalPrefixOptionalWhenFormatting) {
String carrierCodeFormattingRule = "";
if (element.hasAttribute(CARRIER_CODE_FORMATTING_RULE)) {
carrierCodeFormattingRule = validateRE(
getDomesticCarrierCodeFormattingRuleFromElement(element, nationalPrefix));
}
NodeList numberFormatElements = element.getElementsByTagName(NUMBER_FORMAT);
boolean hasExplicitIntlFormatDefined = false;
int numOfFormatElements = numberFormatElements.getLength();
if (numOfFormatElements > 0) {
for (int i = 0; i < numOfFormatElements; i++) {
Element numberFormatElement = (Element) numberFormatElements.item(i);
NumberFormat.Builder format = NumberFormat.newBuilder();
if (numberFormatElement.hasAttribute(NATIONAL_PREFIX_FORMATTING_RULE)) {
format.setNationalPrefixFormattingRule(
getNationalPrefixFormattingRuleFromElement(numberFormatElement, nationalPrefix));
} else if (!nationalPrefixFormattingRule.equals("")) {
format.setNationalPrefixFormattingRule(nationalPrefixFormattingRule);
}
if (numberFormatElement.hasAttribute(NATIONAL_PREFIX_OPTIONAL_WHEN_FORMATTING)) {
format.setNationalPrefixOptionalWhenFormatting(
Boolean.valueOf(numberFormatElement.getAttribute(
NATIONAL_PREFIX_OPTIONAL_WHEN_FORMATTING)));
} else if (format.getNationalPrefixOptionalWhenFormatting()
!= nationalPrefixOptionalWhenFormatting) {
// Inherit from the parent field if it is not already the same as the default.
format.setNationalPrefixOptionalWhenFormatting(nationalPrefixOptionalWhenFormatting);
}
if (numberFormatElement.hasAttribute(CARRIER_CODE_FORMATTING_RULE)) {
format.setDomesticCarrierCodeFormattingRule(validateRE(
getDomesticCarrierCodeFormattingRuleFromElement(numberFormatElement,
nationalPrefix)));
} else if (!carrierCodeFormattingRule.equals("")) {
format.setDomesticCarrierCodeFormattingRule(carrierCodeFormattingRule);
}
loadNationalFormat(metadata, numberFormatElement, format);
metadata.addNumberFormat(format);
if (loadInternationalFormat(metadata, numberFormatElement, format.build())) {
hasExplicitIntlFormatDefined = true;
}
}
// Only a small number of regions need to specify the intlFormats in the xml. For the majority
// of countries the intlNumberFormat metadata is an exact copy of the national NumberFormat
// metadata. To minimize the size of the metadata file, we only keep intlNumberFormats that
// actually differ in some way to the national formats.
if (!hasExplicitIntlFormatDefined) {
metadata.clearIntlNumberFormat();
}
}
}
// @VisibleForTesting
static void setLeadingDigitsPatterns(Element numberFormatElement, NumberFormat.Builder format) {
NodeList leadingDigitsPatternNodes = numberFormatElement.getElementsByTagName(LEADING_DIGITS);
int numOfLeadingDigitsPatterns = leadingDigitsPatternNodes.getLength();
if (numOfLeadingDigitsPatterns > 0) {
for (int i = 0; i < numOfLeadingDigitsPatterns; i++) {
format.addLeadingDigitsPattern(
validateRE((leadingDigitsPatternNodes.item(i)).getFirstChild().getNodeValue(), true));
}
}
}
// @VisibleForTesting
static String getNationalPrefixFormattingRuleFromElement(Element element,
String nationalPrefix) {
String nationalPrefixFormattingRule = element.getAttribute(NATIONAL_PREFIX_FORMATTING_RULE);
// Replace $NP with national prefix and $FG with the first group ($1).
nationalPrefixFormattingRule =
nationalPrefixFormattingRule.replaceFirst("\\$NP", nationalPrefix)
.replaceFirst("\\$FG", "\\$1");
return nationalPrefixFormattingRule;
}
// @VisibleForTesting
static String getDomesticCarrierCodeFormattingRuleFromElement(Element element,
String nationalPrefix) {
String carrierCodeFormattingRule = element.getAttribute(CARRIER_CODE_FORMATTING_RULE);
// Replace $FG with the first group ($1) and $NP with the national prefix.
carrierCodeFormattingRule = carrierCodeFormattingRule.replaceFirst("\\$FG", "\\$1")
.replaceFirst("\\$NP", nationalPrefix);
return carrierCodeFormattingRule;
}
/**
* Checks if the possible lengths provided as a sorted set are equal to the possible lengths
* stored already in the description pattern. Note that possibleLengths may be empty but must not
* be null, and the PhoneNumberDesc passed in should also not be null.
*/
private static boolean arePossibleLengthsEqual(TreeSet<Integer> possibleLengths,
PhoneNumberDesc desc) {
if (possibleLengths.size() != desc.getPossibleLengthCount()) {
return false;
}
// Note that both should be sorted already, and we know they are the same length.
int i = 0;
for (Integer length : possibleLengths) {
if (length != desc.getPossibleLength(i)) {
return false;
}
i++;
}
return true;
}
/**
* Processes a phone number description element from the XML file and returns it as a
* PhoneNumberDesc. If the description element is a fixed line or mobile number, the parent
* description will be used to fill in the whole element if necessary, or any components that are
* missing. For all other types, the parent description will only be used to fill in missing
* components if the type has a partial definition. For example, if no "tollFree" element exists,
* we assume there are no toll free numbers for that locale, and return a phone number description
* with no national number data and [-1] for the possible lengths. Note that the parent
* description must therefore already be processed before this method is called on any child
* elements.
*
* @param parentDesc a generic phone number description that will be used to fill in missing
* parts of the description, or null if this is the root node. This must be processed before
* this is run on any child elements.
* @param countryElement the XML element representing all the country information
* @param numberType the name of the number type, corresponding to the appropriate tag in the XML
* file with information about that type
* @return complete description of that phone number type
*/
// @VisibleForTesting
static PhoneNumberDesc.Builder processPhoneNumberDescElement(PhoneNumberDesc.Builder parentDesc,
Element countryElement,
String numberType) {
NodeList phoneNumberDescList = countryElement.getElementsByTagName(numberType);
PhoneNumberDesc.Builder numberDesc = PhoneNumberDesc.newBuilder();
if (phoneNumberDescList.getLength() == 0) {
// -1 will never match a possible phone number length, so is safe to use to ensure this never
// matches. We don't leave it empty, since for compression reasons, we use the empty list to
// mean that the generalDesc possible lengths apply.
numberDesc.addPossibleLength(-1);
return numberDesc;
}
if (phoneNumberDescList.getLength() > 0) {
if (phoneNumberDescList.getLength() > 1) {
throw new RuntimeException(
String.format("Multiple elements with type %s found.", numberType));
}
Element element = (Element) phoneNumberDescList.item(0);
if (parentDesc != null) {
// New way of handling possible number lengths. We don't do this for the general
// description, since these tags won't be present; instead we will calculate its values
// based on the values for all the other number type descriptions (see
// setPossibleLengthsGeneralDesc).
TreeSet<Integer> lengths = new TreeSet<Integer>();
TreeSet<Integer> localOnlyLengths = new TreeSet<Integer>();
populatePossibleLengthSets(element, lengths, localOnlyLengths);
setPossibleLengths(lengths, localOnlyLengths, parentDesc.build(), numberDesc);
}
NodeList validPattern = element.getElementsByTagName(NATIONAL_NUMBER_PATTERN);
if (validPattern.getLength() > 0) {
numberDesc.setNationalNumberPattern(
validateRE(validPattern.item(0).getFirstChild().getNodeValue(), true));
}
NodeList exampleNumber = element.getElementsByTagName(EXAMPLE_NUMBER);
if (exampleNumber.getLength() > 0) {
numberDesc.setExampleNumber(exampleNumber.item(0).getFirstChild().getNodeValue());
}
}
return numberDesc;
}
// @VisibleForTesting
static void setRelevantDescPatterns(PhoneMetadata.Builder metadata, Element element,
boolean isShortNumberMetadata) {
PhoneNumberDesc.Builder generalDesc = processPhoneNumberDescElement(null, element,
GENERAL_DESC);
// Calculate the possible lengths for the general description. This will be based on the
// possible lengths of the child elements.
setPossibleLengthsGeneralDesc(generalDesc, metadata.getId(), element, isShortNumberMetadata);
metadata.setGeneralDesc(generalDesc);
if (!isShortNumberMetadata) {
// Set fields used by regular length phone numbers.
metadata.setFixedLine(processPhoneNumberDescElement(generalDesc, element, FIXED_LINE));
metadata.setMobile(processPhoneNumberDescElement(generalDesc, element, MOBILE));
metadata.setSharedCost(processPhoneNumberDescElement(generalDesc, element, SHARED_COST));
metadata.setVoip(processPhoneNumberDescElement(generalDesc, element, VOIP));
metadata.setPersonalNumber(processPhoneNumberDescElement(generalDesc, element,
PERSONAL_NUMBER));
metadata.setPager(processPhoneNumberDescElement(generalDesc, element, PAGER));
metadata.setUan(processPhoneNumberDescElement(generalDesc, element, UAN));
metadata.setVoicemail(processPhoneNumberDescElement(generalDesc, element, VOICEMAIL));
metadata.setNoInternationalDialling(processPhoneNumberDescElement(generalDesc, element,
NO_INTERNATIONAL_DIALLING));
boolean mobileAndFixedAreSame = metadata.getMobile().getNationalNumberPattern()
.equals(metadata.getFixedLine().getNationalNumberPattern());
if (metadata.getSameMobileAndFixedLinePattern() != mobileAndFixedAreSame) {
// Set this if it is not the same as the default.
metadata.setSameMobileAndFixedLinePattern(mobileAndFixedAreSame);
}
metadata.setTollFree(processPhoneNumberDescElement(generalDesc, element, TOLL_FREE));
metadata.setPremiumRate(processPhoneNumberDescElement(generalDesc, element, PREMIUM_RATE));
} else {
// Set fields used by short numbers.
metadata.setStandardRate(processPhoneNumberDescElement(generalDesc, element, STANDARD_RATE));
metadata.setShortCode(processPhoneNumberDescElement(generalDesc, element, SHORT_CODE));
metadata.setCarrierSpecific(processPhoneNumberDescElement(generalDesc, element,
CARRIER_SPECIFIC));
metadata.setEmergency(processPhoneNumberDescElement(generalDesc, element, EMERGENCY));
metadata.setTollFree(processPhoneNumberDescElement(generalDesc, element, TOLL_FREE));
metadata.setPremiumRate(processPhoneNumberDescElement(generalDesc, element, PREMIUM_RATE));
metadata.setSmsServices(processPhoneNumberDescElement(generalDesc, element, SMS_SERVICES));
}
}
/**
* Parses a possible length string into a set of the integers that are covered.
*
* @param possibleLengthString a string specifying the possible lengths of phone numbers. Follows
* this syntax: ranges or elements are separated by commas, and ranges are specified in
* [min-max] notation, inclusive. For example, [3-5],7,9,[11-14] should be parsed to
* 3,4,5,7,9,11,12,13,14.
*/
private static Set<Integer> parsePossibleLengthStringToSet(String possibleLengthString) {
if (possibleLengthString.length() == 0) {
throw new RuntimeException("Empty possibleLength string found.");
}
String[] lengths = possibleLengthString.split(",");
Set<Integer> lengthSet = new TreeSet<Integer>();
for (int i = 0; i < lengths.length; i++) {
String lengthSubstring = lengths[i];
if (lengthSubstring.length() == 0) {
throw new RuntimeException(String.format("Leading, trailing or adjacent commas in possible "
+ "length string %s, these should only separate numbers or ranges.",
possibleLengthString));
} else if (lengthSubstring.charAt(0) == '[') {
if (lengthSubstring.charAt(lengthSubstring.length() - 1) != ']') {
throw new RuntimeException(String.format("Missing end of range character in possible "
+ "length string %s.", possibleLengthString));
}
// Strip the leading and trailing [], and split on the -.
String[] minMax = lengthSubstring.substring(1, lengthSubstring.length() - 1).split("-");
if (minMax.length != 2) {
throw new RuntimeException(String.format("Ranges must have exactly one - character: "
+ "missing for %s.", possibleLengthString));
}
int min = Integer.parseInt(minMax[0]);
int max = Integer.parseInt(minMax[1]);
// We don't even accept [6-7] since we prefer the shorter 6,7 variant; for a range to be in
// use the hyphen needs to replace at least one digit.
if (max - min < 2) {
throw new RuntimeException(String.format("The first number in a range should be two or "
+ "more digits lower than the second. Culprit possibleLength string: %s",
possibleLengthString));
}
for (int j = min; j <= max; j++) {
if (!lengthSet.add(j)) {
throw new RuntimeException(String.format("Duplicate length element found (%d) in "
+ "possibleLength string %s", j, possibleLengthString));
}
}
} else {
int length = Integer.parseInt(lengthSubstring);
if (!lengthSet.add(length)) {
throw new RuntimeException(String.format("Duplicate length element found (%d) in "
+ "possibleLength string %s", length, possibleLengthString));
}
}
}
return lengthSet;
}
/**
* Reads the possible lengths present in the metadata and splits them into two sets: one for
* full-length numbers, one for local numbers.
*
* @param data one or more phone number descriptions, represented as XML nodes
* @param lengths a set to which to add possible lengths of full phone numbers
* @param localOnlyLengths a set to which to add possible lengths of phone numbers only diallable
* locally (e.g. within a province)
*/
private static void populatePossibleLengthSets(Element data, TreeSet<Integer> lengths,
TreeSet<Integer> localOnlyLengths) {
NodeList possibleLengths = data.getElementsByTagName(POSSIBLE_LENGTHS);
for (int i = 0; i < possibleLengths.getLength(); i++) {
Element element = (Element) possibleLengths.item(i);
String nationalLengths = element.getAttribute(NATIONAL);
// We don't add to the phone metadata yet, since we want to sort length elements found under
// different nodes first, make sure there are no duplicates between them and that the
// localOnly lengths don't overlap with the others.
Set<Integer> thisElementLengths = parsePossibleLengthStringToSet(nationalLengths);
if (element.hasAttribute(LOCAL_ONLY)) {
String localLengths = element.getAttribute(LOCAL_ONLY);
Set<Integer> thisElementLocalOnlyLengths = parsePossibleLengthStringToSet(localLengths);
Set<Integer> intersection = new HashSet<Integer>(thisElementLengths);
intersection.retainAll(thisElementLocalOnlyLengths);
if (!intersection.isEmpty()) {
throw new RuntimeException(String.format(
"Possible length(s) found specified as a normal and local-only length: %s",
intersection));
}
// We check again when we set these lengths on the metadata itself in setPossibleLengths
// that the elements in localOnly are not also in lengths. For e.g. the generalDesc, it
// might have a local-only length for one type that is a normal length for another type. We
// don't consider this an error, but we do want to remove the local-only lengths.
localOnlyLengths.addAll(thisElementLocalOnlyLengths);
}
// It is okay if at this time we have duplicates, because the same length might be possible
// for e.g. fixed-line and for mobile numbers, and this method operates potentially on
// multiple phoneNumberDesc XML elements.
lengths.addAll(thisElementLengths);
}
}
/**
* Sets possible lengths in the general description, derived from certain child elements.
*/
// @VisibleForTesting
static void setPossibleLengthsGeneralDesc(PhoneNumberDesc.Builder generalDesc, String metadataId,
Element data, boolean isShortNumberMetadata) {
TreeSet<Integer> lengths = new TreeSet<Integer>();
TreeSet<Integer> localOnlyLengths = new TreeSet<Integer>();
// The general description node should *always* be present if metadata for other types is
// present, aside from in some unit tests.
// (However, for e.g. formatting metadata in PhoneNumberAlternateFormats, no PhoneNumberDesc
// elements are present).
NodeList generalDescNodes = data.getElementsByTagName(GENERAL_DESC);
if (generalDescNodes.getLength() > 0) {
Element generalDescNode = (Element) generalDescNodes.item(0);
populatePossibleLengthSets(generalDescNode, lengths, localOnlyLengths);
if (!lengths.isEmpty() || !localOnlyLengths.isEmpty()) {
// We shouldn't have anything specified at the "general desc" level: we are going to
// calculate this ourselves from child elements.
throw new RuntimeException(String.format("Found possible lengths specified at general "
+ "desc: this should be derived from child elements. Affected country: %s",
metadataId));
}
}
if (!isShortNumberMetadata) {
// Make a copy here since we want to remove some nodes, but we don't want to do that on our
// actual data.
Element allDescData = (Element) data.cloneNode(true /* deep copy */);
for (String tag : PHONE_NUMBER_DESCS_WITHOUT_MATCHING_TYPES) {
NodeList nodesToRemove = allDescData.getElementsByTagName(tag);
if (nodesToRemove.getLength() > 0) {
// We check when we process phone number descriptions that there are only one of each
// type, so this is safe to do.
allDescData.removeChild(nodesToRemove.item(0));
}
}
populatePossibleLengthSets(allDescData, lengths, localOnlyLengths);
} else {
// For short number metadata, we want to copy the lengths from the "short code" section only.
// This is because it's the more detailed validation pattern, it's not a sub-type of short
// codes. The other lengths will be checked later to see that they are a sub-set of these
// possible lengths.
NodeList shortCodeDescList = data.getElementsByTagName(SHORT_CODE);
if (shortCodeDescList.getLength() > 0) {
Element shortCodeDesc = (Element) shortCodeDescList.item(0);
populatePossibleLengthSets(shortCodeDesc, lengths, localOnlyLengths);
}
if (localOnlyLengths.size() > 0) {
throw new RuntimeException("Found local-only lengths in short-number metadata");
}
}
setPossibleLengths(lengths, localOnlyLengths, null, generalDesc);
}
/**
* Sets the possible length fields in the metadata from the sets of data passed in. Checks that
* the length is covered by the "parent" phone number description element if one is present, and
* if the lengths are exactly the same as this, they are not filled in for efficiency reasons.
*
* @param parentDesc the "general description" element or null if desc is the generalDesc itself
* @param desc the PhoneNumberDesc object that we are going to set lengths for
*/
private static void setPossibleLengths(TreeSet<Integer> lengths,
TreeSet<Integer> localOnlyLengths, PhoneNumberDesc parentDesc, PhoneNumberDesc.Builder desc) {
// We clear these fields since the metadata tends to inherit from the parent element for other
// fields (via a mergeFrom).
desc.clearPossibleLength();
desc.clearPossibleLengthLocalOnly();
// Only add the lengths to this sub-type if they aren't exactly the same as the possible
// lengths in the general desc (for metadata size reasons).
if (parentDesc == null || !arePossibleLengthsEqual(lengths, parentDesc)) {
for (Integer length : lengths) {
if (parentDesc == null || parentDesc.getPossibleLengthList().contains(length)) {
desc.addPossibleLength(length);
} else {
// We shouldn't have possible lengths defined in a child element that are not covered by
// the general description. We check this here even though the general description is
// derived from child elements because it is only derived from a subset, and we need to
// ensure *all* child elements have a valid possible length.
throw new RuntimeException(String.format(
"Out-of-range possible length found (%d), parent lengths %s.",
length, parentDesc.getPossibleLengthList()));
}
}
}
// We check that the local-only length isn't also a normal possible length (only relevant for
// the general-desc, since within elements such as fixed-line we would throw an exception if we
// saw this) before adding it to the collection of possible local-only lengths.
for (Integer length : localOnlyLengths) {
if (!lengths.contains(length)) {
// We check it is covered by either of the possible length sets of the parent
// PhoneNumberDesc, because for example 7 might be a valid localOnly length for mobile, but
// a valid national length for fixedLine, so the generalDesc would have the 7 removed from
// localOnly.
if (parentDesc == null || parentDesc.getPossibleLengthLocalOnlyList().contains(length)
|| parentDesc.getPossibleLengthList().contains(length)) {
desc.addPossibleLengthLocalOnly(length);
} else {
throw new RuntimeException(String.format(
"Out-of-range local-only possible length found (%d), parent length %s.",
length, parentDesc.getPossibleLengthLocalOnlyList()));
}
}
}
}
// @VisibleForTesting
static PhoneMetadata.Builder loadCountryMetadata(String regionCode,
Element element,
boolean isShortNumberMetadata,
boolean isAlternateFormatsMetadata) {
String nationalPrefix = getNationalPrefix(element);
PhoneMetadata.Builder metadata = loadTerritoryTagMetadata(regionCode, element, nationalPrefix);
String nationalPrefixFormattingRule =
getNationalPrefixFormattingRuleFromElement(element, nationalPrefix);
loadAvailableFormats(metadata, element, nationalPrefix,
nationalPrefixFormattingRule,
element.hasAttribute(NATIONAL_PREFIX_OPTIONAL_WHEN_FORMATTING));
if (!isAlternateFormatsMetadata) {
// The alternate formats metadata does not need most of the patterns to be set.
setRelevantDescPatterns(metadata, element, isShortNumberMetadata);
}
return metadata;
}
/**
* Processes the custom build flags and gets a {@code MetadataFilter} which may be used to
* filter {@code PhoneMetadata} objects. Incompatible flag combinations throw RuntimeException.
*
* @param liteBuild The liteBuild flag value as given by the command-line
* @param specialBuild The specialBuild flag value as given by the command-line
*/
// @VisibleForTesting
static MetadataFilter getMetadataFilter(boolean liteBuild, boolean specialBuild) {
if (specialBuild) {
if (liteBuild) {
throw new RuntimeException("liteBuild and specialBuild may not both be set");
}
return MetadataFilter.forSpecialBuild();
}
if (liteBuild) {
return MetadataFilter.forLiteBuild();
}
return MetadataFilter.emptyFilter();
}
}

View File

@@ -0,0 +1,51 @@
/*
* Copyright (C) 2011 The Libphonenumber Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.i18n.phonenumbers;
/**
* Abstract class defining a common interface for commands provided by build tools (e.g: commands to
* generate code or to download source files).
*
* <p> Subclass it to create a new command (e.g: code generation step).
*
* @author Philippe Liard
*/
public abstract class Command {
// The arguments provided to this command. The first one is the name of the command.
private String[] args;
/**
* Entry point of the command called by the CommandDispatcher when requested. This method must be
* implemented by subclasses.
*/
public abstract boolean start();
/**
* The name of the command is used by the CommandDispatcher to execute the requested command. The
* Dispatcher will pass along all command-line arguments to this command, so args[0] will be
* always the command name.
*/
public abstract String getCommandName();
public String[] getArgs() {
return args;
}
public void setArgs(String[] args) {
this.args = args;
}
}

View File

@@ -0,0 +1,78 @@
/*
* Copyright (C) 2011 The Libphonenumber Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.i18n.phonenumbers;
/**
* This class is designed to execute a requested command among a set of provided commands.
* The dispatching is performed according to the requested command name, which is provided as the
* first string of the 'args' array. The 'args' array also contains the command arguments available
* from position 1 to end. The verification of the arguments' consistency is under the
* responsibility of the command since the dispatcher can't be aware of its underlying goals.
*
* @see Command
* @author Philippe Liard
*/
public class CommandDispatcher {
// Command line arguments passed to the command which will be executed. Note that the first one is
// the name of the command.
private final String[] args;
// Supported commands by this dispatcher.
private final Command[] commands;
public CommandDispatcher(String[] args, Command[] commands) {
this.args = args;
this.commands = commands;
}
/**
* Executes the command named `args[0]` if any. If the requested command (in args[0]) is not
* supported, display a help message.
*
* <p> Note that the command name comparison is case sensitive.
*/
public boolean start() {
if (args.length != 0) {
String requestedCommand = args[0];
for (Command command : commands) {
if (command.getCommandName().equals(requestedCommand)) {
command.setArgs(args);
return command.start();
}
}
}
displayUsage();
return false;
}
/**
* Displays a message containing the list of the supported commands by this dispatcher.
*/
private void displayUsage() {
StringBuilder msg = new StringBuilder("Usage: java -jar /path/to/jar [ ");
int i = 0;
for (Command command : commands) {
msg.append(command.getCommandName());
if (i++ != commands.length - 1) {
msg.append(" | ");
}
}
msg.append(" ] args");
System.err.println(msg.toString());
}
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright (C) 2011 The Libphonenumber Authors
* Copyright (C) %d Vladislav Kashin (modified)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.i18n.phonenumbers;
import java.io.IOException;
import java.io.Writer;
import java.util.Formatter;
/**
* Class containing the Apache copyright notice used by code generators.
*
* @author Philippe Liard
* @author Kashin Vladislav (modified for Rust code generation)
*/
public class CopyrightNotice {
private static final String TEXT_OPENING =
"/*\n";
private static final String TEXT =
" * Copyright (C) %d The Libphonenumber Authors\n" +
" * Copyright (C) %d Vladislav Kashin (modified)\n" +
" *\n" +
" * Licensed under the Apache License, Version 2.0 (the \"License\");\n" +
" * you may not use this file except in compliance with the License.\n" +
" * You may obtain a copy of the License at\n" +
" *\n" +
" * http://www.apache.org/licenses/LICENSE-2.0\n" +
" *\n" +
" * Unless required by applicable law or agreed to in writing, software\n" +
" * distributed under the License is distributed on an \"AS IS\" BASIS,\n" +
" * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n" +
" * See the License for the specific language governing permissions and\n" +
" * limitations under the License.\n" +
" */\n\n";
static final void writeTo(Writer writer, int year, int yearSecondAuthor) throws IOException {
writer.write(TEXT_OPENING);
Formatter formatter = new Formatter(writer);
formatter.format(TEXT, year, yearSecondAuthor);
}
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright (C) 2011 The Libphonenumber Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* under the License.
*/
package com.google.i18n.phonenumbers;
import java.io.Closeable;
import java.io.IOException;
/**
* Helper class containing methods designed to ease file manipulation and generation.
*
* @author Philippe Liard
*/
public class FileUtils {
/**
* Silently closes a resource (i.e: don't throw any exception).
*/
private static void close(Closeable closeable) {
if (closeable == null) {
return;
}
try {
closeable.close();
} catch (IOException e) {
System.err.println(e.getMessage());
}
}
/**
* Silently closes multiple resources. This method doesn't throw any exception when an error
* occurs when a resource is being closed.
*/
public static void closeFiles(Closeable ... closeables) {
for (Closeable closeable : closeables) {
close(closeable);
}
}
}

View File

@@ -0,0 +1,356 @@
/*
* Copyright (C) 2016 The Libphonenumber Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.i18n.phonenumbers;
import com.google.i18n.phonenumbers.Phonemetadata.PhoneMetadata;
import com.google.i18n.phonenumbers.Phonemetadata.PhoneNumberDesc;
import java.util.Arrays;
import java.util.TreeMap;
import java.util.TreeSet;
/**
* Class to encapsulate the metadata filtering logic and restrict visibility into raw data
* structures.
*
* <p>
* WARNING: This is an internal API which is under development and subject to backwards-incompatible
* changes without notice. Any changes are not guaranteed to be reflected in the versioning scheme
* of the public API, nor in release notes.
*/
final class MetadataFilter {
// The following 3 sets comprise all the PhoneMetadata fields as defined at phonemetadata.proto
// which may be excluded from customized serializations of the binary metadata. Fields that are
// core to the library functionality may not be listed here.
// excludableParentFields are PhoneMetadata fields of type PhoneNumberDesc.
// excludableChildFields are PhoneNumberDesc fields of primitive type.
// excludableChildlessFields are PhoneMetadata fields of primitive type.
// Currently we support only one non-primitive type and the depth of the "family tree" is 2,
// meaning a field may have only direct descendants, who may not have descendants of their own. If
// this changes, the blacklist handling in this class should also change.
// @VisibleForTesting
static final TreeSet<String> excludableParentFields = new TreeSet<String>(Arrays.asList(
"fixedLine",
"mobile",
"tollFree",
"premiumRate",
"sharedCost",
"personalNumber",
"voip",
"pager",
"uan",
"emergency",
"voicemail",
"shortCode",
"standardRate",
"carrierSpecific",
"smsServices",
"noInternationalDialling"));
// Note: If this set changes, the descHasData implementation must change in PhoneNumberUtil.
// The current implementation assumes that all PhoneNumberDesc fields are present here, since it
// "clears" a PhoneNumberDesc field by simply clearing all of the fields under it. See the comment
// above, about all 3 sets, for more about these fields.
// @VisibleForTesting
static final TreeSet<String> excludableChildFields = new TreeSet<String>(Arrays.asList(
"nationalNumberPattern",
"possibleLength",
"possibleLengthLocalOnly",
"exampleNumber"));
// @VisibleForTesting
static final TreeSet<String> excludableChildlessFields = new TreeSet<String>(Arrays.asList(
"preferredInternationalPrefix",
"nationalPrefix",
"preferredExtnPrefix",
"nationalPrefixTransformRule",
"sameMobileAndFixedLinePattern",
"mainCountryForCode",
"mobileNumberPortableRegion"));
private final TreeMap<String, TreeSet<String>> blacklist;
// Note: If changing the blacklist here or the name of the method, update documentation about
// affected methods at the same time:
// https://github.com/google/libphonenumber/blob/master/FAQ.md#what-is-the-metadatalitejsmetadata_lite-option
static MetadataFilter forLiteBuild() {
// "exampleNumber" is a blacklist.
return new MetadataFilter(parseFieldMapFromString("exampleNumber"));
}
static MetadataFilter forSpecialBuild() {
// "mobile" is a whitelist.
return new MetadataFilter(computeComplement(parseFieldMapFromString("mobile")));
}
static MetadataFilter emptyFilter() {
// Empty blacklist, meaning we filter nothing.
return new MetadataFilter(new TreeMap<String, TreeSet<String>>());
}
// @VisibleForTesting
MetadataFilter(TreeMap<String, TreeSet<String>> blacklist) {
this.blacklist = blacklist;
}
@Override
public boolean equals(Object obj) {
return blacklist.equals(((MetadataFilter) obj).blacklist);
}
@Override
public int hashCode() {
return blacklist.hashCode();
}
/**
* Clears certain fields in {@code metadata} as defined by the {@code MetadataFilter} instance.
* Note that this changes the mutable {@code metadata} object, and is not thread-safe. If this
* method does not return successfully, do not assume {@code metadata} has not changed.
*
* @param metadata The {@code PhoneMetadata} object to be filtered
*/
void filterMetadata(PhoneMetadata.Builder metadata) {
// TODO: Consider clearing if the filtered PhoneNumberDesc is empty.
if (metadata.hasFixedLine()) {
metadata.setFixedLine(getFiltered("fixedLine", metadata.getFixedLine()));
}
if (metadata.hasMobile()) {
metadata.setMobile(getFiltered("mobile", metadata.getMobile()));
}
if (metadata.hasTollFree()) {
metadata.setTollFree(getFiltered("tollFree", metadata.getTollFree()));
}
if (metadata.hasPremiumRate()) {
metadata.setPremiumRate(getFiltered("premiumRate", metadata.getPremiumRate()));
}
if (metadata.hasSharedCost()) {
metadata.setSharedCost(getFiltered("sharedCost", metadata.getSharedCost()));
}
if (metadata.hasPersonalNumber()) {
metadata.setPersonalNumber(getFiltered("personalNumber", metadata.getPersonalNumber()));
}
if (metadata.hasVoip()) {
metadata.setVoip(getFiltered("voip", metadata.getVoip()));
}
if (metadata.hasPager()) {
metadata.setPager(getFiltered("pager", metadata.getPager()));
}
if (metadata.hasUan()) {
metadata.setUan(getFiltered("uan", metadata.getUan()));
}
if (metadata.hasEmergency()) {
metadata.setEmergency(getFiltered("emergency", metadata.getEmergency()));
}
if (metadata.hasVoicemail()) {
metadata.setVoicemail(getFiltered("voicemail", metadata.getVoicemail()));
}
if (metadata.hasShortCode()) {
metadata.setShortCode(getFiltered("shortCode", metadata.getShortCode()));
}
if (metadata.hasStandardRate()) {
metadata.setStandardRate(getFiltered("standardRate", metadata.getStandardRate()));
}
if (metadata.hasCarrierSpecific()) {
metadata.setCarrierSpecific(getFiltered("carrierSpecific", metadata.getCarrierSpecific()));
}
if (metadata.hasSmsServices()) {
metadata.setSmsServices(getFiltered("smsServices", metadata.getSmsServices()));
}
if (metadata.hasNoInternationalDialling()) {
metadata.setNoInternationalDialling(getFiltered("noInternationalDialling",
metadata.getNoInternationalDialling()));
}
if (shouldDrop("preferredInternationalPrefix")) {
metadata.clearPreferredInternationalPrefix();
}
if (shouldDrop("nationalPrefix")) {
metadata.clearNationalPrefix();
}
if (shouldDrop("preferredExtnPrefix")) {
metadata.clearPreferredExtnPrefix();
}
if (shouldDrop("nationalPrefixTransformRule")) {
metadata.clearNationalPrefixTransformRule();
}
if (shouldDrop("sameMobileAndFixedLinePattern")) {
metadata.clearSameMobileAndFixedLinePattern();
}
if (shouldDrop("mainCountryForCode")) {
metadata.clearMainCountryForCode();
}
if (shouldDrop("mobileNumberPortableRegion")) {
metadata.clearMobileNumberPortableRegion();
}
}
/**
* The input blacklist or whitelist string is expected to be of the form "a(b,c):d(e):f", where
* b and c are children of a, e is a child of d, and f is either a parent field, a child field, or
* a childless field. Order and whitespace don't matter. We throw RuntimeException for any
* duplicates, malformed strings, or strings where field tokens do not correspond to strings in
* the sets of excludable fields. We also throw RuntimeException for empty strings since such
* strings should be treated as a special case by the flag checking code and not passed here.
*/
// @VisibleForTesting
static TreeMap<String, TreeSet<String>> parseFieldMapFromString(String string) {
if (string == null) {
throw new RuntimeException("Null string should not be passed to parseFieldMapFromString");
}
// Remove whitespace.
string = string.replaceAll("\\s", "");
if (string.isEmpty()) {
throw new RuntimeException("Empty string should not be passed to parseFieldMapFromString");
}
TreeMap<String, TreeSet<String>> fieldMap = new TreeMap<String, TreeSet<String>>();
TreeSet<String> wildcardChildren = new TreeSet<String>();
for (String group : string.split(":", -1)) {
int leftParenIndex = group.indexOf('(');
int rightParenIndex = group.indexOf(')');
if (leftParenIndex < 0 && rightParenIndex < 0) {
if (excludableParentFields.contains(group)) {
if (fieldMap.containsKey(group)) {
throw new RuntimeException(group + " given more than once in " + string);
}
fieldMap.put(group, new TreeSet<String>(excludableChildFields));
} else if (excludableChildlessFields.contains(group)) {
if (fieldMap.containsKey(group)) {
throw new RuntimeException(group + " given more than once in " + string);
}
fieldMap.put(group, new TreeSet<String>());
} else if (excludableChildFields.contains(group)) {
if (wildcardChildren.contains(group)) {
throw new RuntimeException(group + " given more than once in " + string);
}
wildcardChildren.add(group);
} else {
throw new RuntimeException(group + " is not a valid token");
}
} else if (leftParenIndex > 0 && rightParenIndex == group.length() - 1) {
// We don't check for duplicate parentheses or illegal characters since these will be caught
// as not being part of valid field tokens.
String parent = group.substring(0, leftParenIndex);
if (!excludableParentFields.contains(parent)) {
throw new RuntimeException(parent + " is not a valid parent token");
}
if (fieldMap.containsKey(parent)) {
throw new RuntimeException(parent + " given more than once in " + string);
}
TreeSet<String> children = new TreeSet<String>();
for (String child : group.substring(leftParenIndex + 1, rightParenIndex).split(",", -1)) {
if (!excludableChildFields.contains(child)) {
throw new RuntimeException(child + " is not a valid child token");
}
if (!children.add(child)) {
throw new RuntimeException(child + " given more than once in " + group);
}
}
fieldMap.put(parent, children);
} else {
throw new RuntimeException("Incorrect location of parantheses in " + group);
}
}
for (String wildcardChild : wildcardChildren) {
for (String parent : excludableParentFields) {
TreeSet<String> children = fieldMap.get(parent);
if (children == null) {
children = new TreeSet<String>();
fieldMap.put(parent, children);
}
if (!children.add(wildcardChild)
&& fieldMap.get(parent).size() != excludableChildFields.size()) {
// The map already contains parent -> wildcardChild but not all possible children.
// So wildcardChild was given explicitly as a child of parent, which is a duplication
// since it's also given as a wildcard child.
throw new RuntimeException(
wildcardChild + " is present by itself so remove it from " + parent + "'s group");
}
}
}
return fieldMap;
}
// Does not check that legal tokens are used, assuming that fieldMap is constructed using
// parseFieldMapFromString(String) which does check. If fieldMap contains illegal tokens or parent
// fields with no children or other unexpected state, the behavior of this function is undefined.
// @VisibleForTesting
static TreeMap<String, TreeSet<String>> computeComplement(
TreeMap<String, TreeSet<String>> fieldMap) {
TreeMap<String, TreeSet<String>> complement = new TreeMap<String, TreeSet<String>>();
for (String parent : excludableParentFields) {
if (!fieldMap.containsKey(parent)) {
complement.put(parent, new TreeSet<String>(excludableChildFields));
} else {
TreeSet<String> otherChildren = fieldMap.get(parent);
// If the other map has all the children for this parent then we don't want to include the
// parent as a key.
if (otherChildren.size() != excludableChildFields.size()) {
TreeSet<String> children = new TreeSet<String>();
for (String child : excludableChildFields) {
if (!otherChildren.contains(child)) {
children.add(child);
}
}
complement.put(parent, children);
}
}
}
for (String childlessField : excludableChildlessFields) {
if (!fieldMap.containsKey(childlessField)) {
complement.put(childlessField, new TreeSet<String>());
}
}
return complement;
}
// @VisibleForTesting
boolean shouldDrop(String parent, String child) {
if (!excludableParentFields.contains(parent)) {
throw new RuntimeException(parent + " is not an excludable parent field");
}
if (!excludableChildFields.contains(child)) {
throw new RuntimeException(child + " is not an excludable child field");
}
return blacklist.containsKey(parent) && blacklist.get(parent).contains(child);
}
// @VisibleForTesting
boolean shouldDrop(String childlessField) {
if (!excludableChildlessFields.contains(childlessField)) {
throw new RuntimeException(childlessField + " is not an excludable childless field");
}
return blacklist.containsKey(childlessField);
}
private PhoneNumberDesc getFiltered(String type, PhoneNumberDesc desc) {
PhoneNumberDesc.Builder builder = PhoneNumberDesc.newBuilder().mergeFrom(desc);
if (shouldDrop(type, "nationalNumberPattern")) {
builder.clearNationalNumberPattern();
}
if (shouldDrop(type, "possibleLength")) {
builder.clearPossibleLength();
}
if (shouldDrop(type, "possibleLengthLocalOnly")) {
builder.clearPossibleLengthLocalOnly();
}
if (shouldDrop(type, "exampleNumber")) {
builder.clearExampleNumber();
}
return builder.build();
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,93 @@
/*
* Copyright (C) 2016 The Libphonenumber Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.i18n.phonenumbers;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import junit.framework.TestCase;
/**
* Tests to ensure that the {@link MetadataFilter} logic over excludable fields cover all applicable
* fields.
*/
public final class MetadataFilterCoverageTest extends TestCase {
private static final String CODE;
static {
try {
BufferedReader source = new BufferedReader(new InputStreamReader(new BufferedInputStream(
MetadataFilterTest.class.getResourceAsStream(
"/com/google/i18n/phonenumbers/MetadataFilter.java")),
Charset.forName("UTF-8")));
StringBuilder codeBuilder = new StringBuilder();
for (String line = source.readLine(); line != null; line = source.readLine()) {
codeBuilder.append(line).append("\n");
}
CODE = codeBuilder.toString();
} catch (IOException e) {
throw new RuntimeException("MetadataFilter.java resource not set up properly", e);
}
}
public void testCoverageOfExcludableParentFields() {
for (String field : MetadataFilter.excludableParentFields) {
String capitalized = Character.toUpperCase(field.charAt(0)) + field.substring(1);
String conditional = String.format("(?s).*if \\(metadata.has%s\\(\\)\\) \\{\\s+"
+ "metadata.set%s\\(getFiltered\\(\"%s\",\\s+metadata.get%s\\(\\)\\)\\);\\s+\\}.*",
capitalized, capitalized, field, capitalized);
assertTrue("Code is missing correct conditional for " + field, CODE.matches(conditional));
}
assertEquals(countOccurrencesOf("metadata.has", CODE),
MetadataFilter.excludableParentFields.size());
}
public void testCoverageOfExcludableChildFields() {
for (String field : MetadataFilter.excludableChildFields) {
String capitalized = Character.toUpperCase(field.charAt(0)) + field.substring(1);
String conditional = String.format("(?s).*if \\(shouldDrop\\(type, \"%s\"\\)\\) \\{\\s+"
+ "builder.clear%s\\(\\);\\s+\\}.*", field, capitalized);
assertTrue("Code is missing correct conditional for " + field, CODE.matches(conditional));
}
assertEquals(countOccurrencesOf("shouldDrop(type, \"", CODE),
MetadataFilter.excludableChildFields.size());
}
public void testCoverageOfExcludableChildlessFields() {
for (String field : MetadataFilter.excludableChildlessFields) {
String capitalized = Character.toUpperCase(field.charAt(0)) + field.substring(1);
String conditional = String.format("(?s).*if \\(shouldDrop\\(\"%s\"\\)\\) \\{\\s+"
+ "metadata.clear%s\\(\\);\\s+\\}.*", field, capitalized);
assertTrue("Code is missing correct conditional for " + field, CODE.matches(conditional));
}
assertEquals(countOccurrencesOf("shouldDrop(\"", CODE),
MetadataFilter.excludableChildlessFields.size());
}
private static int countOccurrencesOf(String substring, String string) {
int count = 0;
for (int i = string.indexOf(substring); i != -1; i = string.indexOf(substring, i + 1)) {
count++;
}
return count;
}
}

File diff suppressed because it is too large Load Diff

45
tools/java/pom.xml Normal file
View File

@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.google.i18n.phonenumbers</groupId>
<artifactId>tools</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<name>Libphonenumber build tools</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<licenses>
<license>
<name>Apache 2</name>
<url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
<comments>Copyright (C) 2011 The Libphonenumber Authors</comments>
</license>
</licenses>
<profiles>
<profile>
<id>default</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<modules>
<module>common</module>
<module>rust-build</module>
</modules>
</profile>
<profile>
<id>github-actions</id>
<modules>
<module>common</module>
<!-- TODO: Add cpp-build once the protoc dependency or the generated Phonemetadata.java is
hermetic at tools/java/cpp-build/pom.xml. -->
<module>data</module>
<module>java-build</module>
</modules>
</profile>
</profiles>
</project>

View File

@@ -0,0 +1,160 @@
<?xml version="1.0"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>tools</artifactId>
<groupId>com.google.i18n.phonenumbers</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<groupId>com.google.i18n.phonenumbers.tools</groupId>
<artifactId>rust-build</artifactId>
<version>1.0-SNAPSHOT</version>
<name>Libphonenumber Rust build tools</name>
<description>
Rust build tools that download dependencies under base/ from the Chromium source repository, and
generate the Rust metadata code needed to build the libphonenumber library.
It depends on libphonenumber original Java library.
</description>
<build>
<sourceDirectory>src</sourceDirectory>
<testSourceDirectory>test</testSourceDirectory>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>8</source>
<target>8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<!-- Create a directory called 'generated'. -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<version>1.3</version>
<executions>
<execution>
<id>create-generated-directory</id>
<phase>generate-sources</phase>
<configuration>
<tasks>
<mkdir dir="generated"/>
</tasks>
</configuration>
<goals>
<goal>run</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<version>3.4.0</version>
<executions>
<execution>
<id>add-source</id>
<phase>generate-sources</phase>
<goals>
<goal>add-source</goal>
</goals>
<configuration>
<sources>
<!-- Make BuildMetadataFromXml.java available to the source directories. -->
<source>../common/src/</source>
<!-- Make Phonemetadata.java available to the source directories.
BuildMetadataFromXml.java has to work with both
tools/java/cpp-build/generated/com/google/i18n/phonenumbers/Phonemetadata.java
and java/libphonenumber/src/com/google/i18n/phonenumbers/Phonemetadata.java.
TODO: This Phonemetadata.java is generated via a protoc dependency that is not
hermetic and may get out of sync with the other one. Make this file hermetic or
find another way to enable Travis CI on this build. -->
<source>generated/</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
<!-- Invoke Protocol Buffers compiler to generate Phonemetadata.java. -->
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.1.0</version>
<executions>
<execution>
<phase>generate-sources</phase>
<goals>
<goal>exec</goal>
</goals>
</execution>
</executions>
<configuration>
<executable>protoc</executable>
<arguments>
<argument>--java_out=generated</argument>
<argument>../../../resources/phonemetadata.proto</argument>
<argument>--proto_path=../../../resources</argument>
</arguments>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.3.0</version>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<mainClass>com.google.i18n.phonenumbers.EntryPoint</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<mainClass>com.google.i18n.phonenumbers.EntryPoint</mainClass>
</manifest>
</archive>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>3.25.5</version>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,239 @@
/*
* Copyright (C) 2011 The Libphonenumber Authors
* Copyright (C) 2025 The Kashin Vladislav (modified)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.i18n.phonenumbers;
import com.google.i18n.phonenumbers.RustMetadataGenerator.Type;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* This class generates the Rust code representation of the provided XML metadata file. It lets us
* embed metadata directly in a native binary. We link the object resulting from the compilation of
* the code emitted by this class with the Rust rlibphonenumber library.
*
* @author Philippe Liard
* @author David Beaumont
*
* @author Kashin Vladislav (modified for Rust code generation)
*/
public class BuildMetadataRustFromXml extends Command {
/** An enum encapsulating the variations of metadata that we can produce. */
public enum Variant {
/** The default 'full' variant which contains all the metadata. */
FULL("%s"),
/** The test variant which contains fake data for tests. */
TEST("test_%s"),
/**
* The lite variant contains the same metadata as the full version but excludes any example
* data. This is typically used for clients with space restrictions.
*/
LITE("lite_%s");
private final String template;
private Variant(String template) {
this.template = template;
}
/**
* Returns the basename of the type by adding the name of the current variant. The basename of
* a Type is used to determine the name of the source file in which the metadata is defined.
*
* <p>Note that when the variant is {@link Variant#FULL} this method just returns the type name.
*/
public String getBasename(Type type) {
return String.format(template, type);
}
/**
* Parses metadata variant name. By default (for a name of {@code ""} or {@code null}) we return
* {@link Variant#FULL}, otherwise we match against the variant name (either "test" or "lite").
*/
public static Variant parse(String variantName) {
if ("test".equalsIgnoreCase(variantName)) {
return Variant.TEST;
} else if ("lite".equalsIgnoreCase(variantName)) {
return Variant.LITE;
} else if (variantName == null || variantName.length() == 0) {
return Variant.FULL;
} else {
return null;
}
}
}
/**
* An immutable options class for parsing and representing the command line options for this
* command.
*/
// @VisibleForTesting
static final class Options {
private static final Pattern BASENAME_PATTERN =
Pattern.compile("(?:(test|lite)_)?([a-z_]+)");
private static final Pattern CONSTANT_NAME_PATTERN =
Pattern.compile("--const-name[ =]([a-zA-Z_]+)");
private static final String DEFAULT_METADATA_CONSTANT_NAME = "METADATA";
public static Options parse(String commandName, String[] argsArray) {
ArrayList args = new ArrayList(Arrays.asList(argsArray));
String constantName = DEFAULT_METADATA_CONSTANT_NAME;
if (args.size() == 5) {
for (int i = 0; i < args.size(); i++) {
String arg = args.get(i).toString();
Matcher matcher = CONSTANT_NAME_PATTERN.matcher(arg.toString());
if (matcher.matches()) {
constantName = matcher.group(1);
args.remove(arg);
break;
}
}
}
if (args.size() == 4) {
String inputXmlFilePath = args.get(1).toString();
String outputDirPath = args.get(2).toString();
Matcher basenameMatcher = BASENAME_PATTERN.matcher(args.get(3).toString());
if (basenameMatcher.matches()) {
Variant variant = Variant.parse(basenameMatcher.group(1));
Type type = Type.parse(basenameMatcher.group(2));
if (type != null && variant != null) {
return new Options(inputXmlFilePath, outputDirPath, type, variant, constantName);
}
}
}
throw new IllegalArgumentException(String.format(
"Usage: %s <inputXmlFile> <outputDir> <output ( <type> | test_<type> | lite_<type> ) " +
"[--const-name <nameOfMetadataConstant>] \n" +
" where <type> is one of: %s",
commandName, Arrays.asList(Type.values())));
}
// File path where the XML input can be found.
private final String inputXmlFilePath;
// Output directory where the generated files will be saved.
private final String outputDirPath;
private final Type type;
private final Variant variant;
private final String constantName;
private Options(String inputXmlFilePath, String outputDirPath, Type type, Variant variant, String constantName) {
this.inputXmlFilePath = inputXmlFilePath;
this.outputDirPath = outputDirPath;
this.type = type;
this.variant = variant;
this.constantName = constantName;
}
public String getInputFilePath() {
return inputXmlFilePath;
}
public String getOutputDir() {
return outputDirPath;
}
public Type getType() {
return type;
}
public Variant getVariant() {
return variant;
}
public String getConstantName() {
return constantName;
}
}
@Override
public String getCommandName() {
return "BuildMetadataRustFromXml";
}
/**
* Generates Rust source file to represent the metadata specified by this command's
* arguments. The metadata XML file is read and converted to a byte array before being written
* into a Rust source file as a static data array.
*
* @return true if the generation succeeded.
*/
@Override
public boolean start() {
try {
Options opt = Options.parse(getCommandName(), getArgs());
byte[] data = loadMetadataBytes(opt.getInputFilePath(), opt.getVariant() == Variant.LITE);
RustMetadataGenerator metadata = RustMetadataGenerator.create(opt.getType(), data, opt.constantName);
// TODO: Consider adding checking for correctness of file paths and access.
OutputStream headerStream = null;
OutputStream sourceStream = null;
try {
File dir = new File(opt.getOutputDir());
sourceStream = openSourceStream(dir);
metadata.outputSourceFile(new OutputStreamWriter(sourceStream, UTF_8));
} finally {
FileUtils.closeFiles(headerStream, sourceStream);
}
return true;
} catch (IOException e) {
System.err.println(e.getMessage());
} catch (RuntimeException e) {
System.err.println(e.getMessage());
}
return false;
}
/** Loads the metadata XML file and converts its contents to a byte array. */
private byte[] loadMetadataBytes(String inputFilePath, boolean liteMetadata) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
try {
writePhoneMetadataCollection(inputFilePath, liteMetadata, out);
} catch (Exception e) {
// We cannot recover from any exceptions thrown here, so promote them to runtime exceptions.
throw new RuntimeException(e);
} finally {
FileUtils.closeFiles(out);
}
return out.toByteArray();
}
// @VisibleForTesting
void writePhoneMetadataCollection(
String inputFilePath, boolean liteMetadata, OutputStream out) throws IOException, Exception {
BuildMetadataFromXml.buildPhoneMetadataCollection(inputFilePath, liteMetadata, false)
.writeTo(out);
}
// @VisibleForTesting
OutputStream openSourceStream(File file) throws FileNotFoundException {
return new FileOutputStream(file);
}
/** The charset in which our source and header files will be written. */
private static final Charset UTF_8 = Charset.forName("UTF-8");
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright (C) 2011 The Libphonenumber Authors
* Copyright (C) 2025 The Kashin Vladislav (modified)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.i18n.phonenumbers;
/**
* Entry point class for C++ build tools.
*
* @author Philippe Liard
*
* @author Kashin Vladislav (modified for Rust code generation)
*/
public class EntryPoint {
public static void main(String[] args) {
boolean status = new CommandDispatcher(args, new Command[] {
new BuildMetadataRustFromXml()
}).start();
System.exit(status ? 0 : 1);
}
}

View File

@@ -0,0 +1,143 @@
/*
* Copyright (C) 2012 The Libphonenumber Authors
* Copyright (C) 2025 The Kashin Vladislav (modified)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.i18n.phonenumbers;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.Writer;
import java.util.Locale;
/**
* Encapsulation of binary metadata created from XML to be included as static data in C++ source
* files.
*
* @author David Beaumont
* @author Philippe Liard
*
* @author Kashin Vladislav (modified for Rust code generation)
*/
public final class RustMetadataGenerator {
/**
* The metadata type represents the known types of metadata and includes additional information
* such as the copyright year. It is expected that the generated files will be named after the
* {@link #toString} of their type.
*/
public enum Type {
/** The basic phone number metadata (expected to be written to metadata.[h/cc]). */
METADATA(2011, 2025),
/** The alternate format metadata (expected to be written to alternate_format.[h/cc]). */
ALTERNATE_FORMAT(2012, 2025),
/** Metadata for short numbers (expected to be written to short_metadata.[h/cc]). */
SHORT_NUMBERS(2013, 2025);
private final int copyrightYear;
private final int copyrightSecondYear;
private Type(int copyrightYear, int CopyrightSecondYear) {
this.copyrightYear = copyrightYear;
this.copyrightSecondYear = CopyrightSecondYear;
}
/** Returns the year in which this metadata type was first introduced. */
public int getCopyrightYear() {
return copyrightYear;
}
/** Returns the year in which this metadata type was modified for RUST. */
public int getCopyrightSecondYear() {
return copyrightSecondYear;
}
/**
* Parses the type from a string case-insensitively.
*
* @return the matching Type instance or null if not matched.
*/
public static Type parse(String typeName) {
if (Type.METADATA.toString().equalsIgnoreCase(typeName)) {
return Type.METADATA;
} else if (Type.ALTERNATE_FORMAT.toString().equalsIgnoreCase(typeName)) {
return Type.ALTERNATE_FORMAT;
} else if (Type.SHORT_NUMBERS.toString().equalsIgnoreCase(typeName)) {
return Type.SHORT_NUMBERS;
} else {
return null;
}
}
}
/**
* Creates a metadata instance that can write C++ source and header files to represent this given
* byte array as a static unsigned char array. Note that a direct reference to the byte[] is
* retained by the newly created CppXmlMetadata instance, so the caller should treat the array as
* immutable after making this call.
*/
public static RustMetadataGenerator create(Type type, byte[] data, String constantName) {
return new RustMetadataGenerator(type, data, constantName);
}
private final Type type;
private final byte[] data;
private final String constantName;
private RustMetadataGenerator(Type type, byte[] data, String variableName) {
this.type = type;
this.data = data;
this.constantName = variableName;
}
/**
* Writes the source file for the Rust representation of the metadata - a static array
* containing the data itself, to the given writer. Note that this method does not close the given
* writer.
*/
public void outputSourceFile(Writer out) throws IOException {
// TODO: Consider outputting a load method to return the parsed proto directly.
String dataLength = String.valueOf(data.length);
PrintWriter pw = new PrintWriter(out);
CopyrightNotice.writeTo(pw, type.getCopyrightYear(), type.getCopyrightSecondYear());
pw.println("pub const "+constantName+": [u8; "+dataLength+"] = [");
emitStaticArrayData(pw, data);
pw.println("];");
pw.flush();
}
/** Emits the Rust code corresponding to the binary metadata as a static byte array. */
// @VisibleForTesting
static void emitStaticArrayData(PrintWriter pw, byte[] data) {
String separator = " ";
for (int i = 0; i < data.length; i++) {
pw.print(separator);
emitHexByte(pw, data[i]);
separator = ((i + 1) % 13 == 0) ? ",\n " : ", ";
}
pw.println();
}
/** Emits a single byte in the form 0xHH, where H is an upper case hex digit in [0-9A-F]. */
private static void emitHexByte(PrintWriter pw, byte v) {
pw.print("0x");
pw.print(UPPER_HEX[(v & 0xF0) >>> 4]);
pw.print(UPPER_HEX[v & 0xF]);
}
private static final char[] UPPER_HEX = "0123456789ABCDEF".toCharArray();
}

View File

@@ -0,0 +1,144 @@
/*
* Copyright (C) 2011 The Libphonenumber Authors
* Copyright (C) 2025 The Kashin Vladislav (modified)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.i18n.phonenumbers;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import com.google.i18n.phonenumbers.BuildMetadataRustFromXml.Options;
import com.google.i18n.phonenumbers.BuildMetadataRustFromXml.Variant;
import com.google.i18n.phonenumbers.RustMetadataGenerator.Type;
import org.junit.Test;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.OutputStream;
import java.nio.charset.Charset;
/**
* Tests the BuildMetadataCppFromXml implementation to make sure it parses command line options and
* generates code correctly.
*/
public class BuildMetadataRustFromXmlTest {
// Various repeated test strings and data.
private static final String IGNORED = "IGNORED";
private static final String OUTPUT_DIR = "output/dir";
private static final String INPUT_PATH_XML = "input/path.xml";
private static final byte[] TEST_DATA =
new byte[] { (byte) 0xCA, (byte) 0xFE, (byte) 0xBA, (byte) 0xBE };
private static final int TEST_DATA_LEN = TEST_DATA.length;
private static final String TEST_CONSTANT_NAME = "METADATA";
private static final String OUTPUT_DATA = "0xCA, 0xFE, 0xBA, 0xBE";
@Test
public void parseVariant() {
assertNull(Variant.parse("xxx"));
assertEquals(Variant.FULL, Variant.parse(null));
assertEquals(Variant.FULL, Variant.parse(""));
assertEquals(Variant.LITE, Variant.parse("lite"));
assertEquals(Variant.TEST, Variant.parse("test"));
assertEquals(Variant.LITE, Variant.parse("LITE"));
assertEquals(Variant.TEST, Variant.parse("Test"));
}
@Test
public void parseBadOptions() {
try {
BuildMetadataRustFromXml.Options.parse("MyCommand", new String[] { IGNORED });
fail("Expected exception not thrown");
} catch (IllegalArgumentException e) {
assertTrue(e.getMessage().contains("MyCommand"));
}
}
@Test
public void parseGoodOptions() {
Options opt = BuildMetadataRustFromXml.Options.parse("MyCommand",
new String[] { IGNORED, INPUT_PATH_XML, OUTPUT_DIR, "test_alternate_format", "--const-name=" + TEST_CONSTANT_NAME });
assertEquals(Type.ALTERNATE_FORMAT, opt.getType());
assertEquals(Variant.TEST, opt.getVariant());
assertEquals(INPUT_PATH_XML, opt.getInputFilePath());
assertEquals(OUTPUT_DIR, opt.getOutputDir());
assertEquals(TEST_CONSTANT_NAME, opt.getConstantName());
}
@Test
public void generateMetadata() {
String[] args = new String[] {
IGNORED, INPUT_PATH_XML, OUTPUT_DIR, "metadata", "--const-name " + TEST_CONSTANT_NAME };
// Most of the useful asserts are done in the mock class.
MockedCommand command = new MockedCommand(
INPUT_PATH_XML, false, OUTPUT_DIR, Type.METADATA, Variant.FULL, TEST_CONSTANT_NAME
);
command.setArgs(args);
command.start();
// Sanity check the captured data (asserting implicitly that the mocked methods were called).
String sourceString = command.capturedSourceFile();
assertTrue(sourceString.contains("pub const "+TEST_CONSTANT_NAME+": [u8; " + TEST_DATA_LEN + "] ="));
assertTrue(sourceString.contains(OUTPUT_DATA));
assertTrue(sourceString.contains("];"));
}
// no need test for metadata with other names since it's set with parameter
/**
* Manually mocked subclass of BuildMetadataCppFromXml which overrides all file related behavior
* while asserting the validity of any parameters passed to the mocked methods. After starting
* this command, the captured header and source file contents can be retrieved for testing.
*/
static class MockedCommand extends BuildMetadataRustFromXml {
private static final Charset UTF_8 = Charset.forName("UTF-8");
private final String expectedInputFilePath;
private final boolean expectedLiteMetadata;
private final String expectedOutputDirPath;
private final Type expectedType;
private final Variant expectedVariant;
private final String expectedConstantName;
private final ByteArrayOutputStream sourceOut = new ByteArrayOutputStream();
public MockedCommand(String expectedInputFilePath, boolean expectedLiteMetadata,
String expectedOutputDirPath, Type expectedType, Variant expectedVariant,
String expectedConstantName) {
this.expectedInputFilePath = expectedInputFilePath;
this.expectedLiteMetadata = expectedLiteMetadata;
this.expectedOutputDirPath = expectedOutputDirPath;
this.expectedType = expectedType;
this.expectedConstantName = expectedConstantName;
this.expectedVariant = expectedVariant;
}
@Override void writePhoneMetadataCollection(
String inputFilePath, boolean liteMetadata, OutputStream out) throws Exception {
assertEquals(expectedInputFilePath, inputFilePath);
assertEquals(expectedLiteMetadata, liteMetadata);
out.write(TEST_DATA, 0, TEST_DATA.length);
}
@Override OutputStream openSourceStream(File dir) {
assertEquals(expectedOutputDirPath, dir.getPath());
return sourceOut;
}
String capturedSourceFile() {
return new String(sourceOut.toByteArray(), UTF_8);
}
}
}

View File

@@ -0,0 +1,102 @@
/*
* Copyright (C) 2012 The Libphonenumber Authors
* Copyright (C) 2025 The Kashin Vladislav (modified)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.i18n.phonenumbers;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import com.google.i18n.phonenumbers.RustMetadataGenerator.Type;
import org.junit.Test;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* Tests that the CppXmlMetadata class emits the expected source and header files for metadata.
*/
public class RustMetadataGeneratorTest {
// 13 bytes per line, so have 16 bytes to test > 1 line (general case).
// Use all hex digits in both nibbles to test hex formatting.
private static final byte[] TEST_DATA = new byte[] {
(byte) 0xF0, (byte) 0xE1, (byte) 0xD2, (byte) 0xC3,
(byte) 0xB4, (byte) 0xA5, (byte) 0x96, (byte) 0x87,
(byte) 0x78, (byte) 0x69, (byte) 0x5A, (byte) 0x4B,
(byte) 0x3C, (byte) 0x2D, (byte) 0x1E, (byte) 0x0F,
};
private static final int TEST_DATA_LEN = TEST_DATA.length;
private static final String TEST_CONSTANT_NAME = "METADATA";
@Test
public void emitStaticArrayData() {
byte[] data = TEST_DATA;
StringWriter writer = new StringWriter();
RustMetadataGenerator.emitStaticArrayData(new PrintWriter(writer), data);
}
@Test
public void outputSourceFile() throws IOException {
byte[] data = new byte[] { (byte) 0xCA, (byte) 0xFE, (byte) 0xBA, (byte) 0xBE };
String testDataLen = String.valueOf(data.length);
RustMetadataGenerator metadata = RustMetadataGenerator.create(Type.ALTERNATE_FORMAT, data, TEST_CONSTANT_NAME);
StringWriter writer = new StringWriter();
metadata.outputSourceFile(writer);
Iterator<String> lines = toLines(writer.toString()).iterator();
// Sanity check that at least some of the expected lines are present.
assertTrue(consumeUntil("pub const "+TEST_CONSTANT_NAME+": [u8; "+testDataLen+"] = [", lines));
assertTrue(consumeUntil(" 0xCA, 0xFE, 0xBA, 0xBE", lines));
assertTrue(consumeUntil("];", lines));
}
/** Converts a string containing newlines into a list of lines. */
private static List<String> toLines(String s) throws IOException {
BufferedReader reader = new BufferedReader(new StringReader(s));
List<String> lines = new ArrayList<String>();
for (String line = reader.readLine(); line != null; line = reader.readLine()) {
lines.add(line);
}
return lines;
}
/**
* Consumes strings from the given iterator until the expected string is reached (it is also
* consumed). If the expected string is not found, the iterator is exhausted and {@code false} is
* returned.
*
* @return true if the expected string was found while consuming the iterator.
*/
private static boolean consumeUntil(String expected, Iterator<String> it) {
while (it.hasNext()) {
if (it.next().equals(expected)) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,33 @@
#!/bin/bash
filedir="./$(dirname "$0")"
javadir="$filedir/../java"
project_home="$filedir/../.."
generated_dir="$project_home/src/phonenumberutil/generated"
echo $generated_dir
resources_dir="$project_home/resources"
rust_build_jar="$javadir/rust-build/target/rust-build-1.0-SNAPSHOT-jar-with-dependencies.jar"
# mvn -f "$javadir/pom.xml" install
mkdir -p "$generated_dir"
function generate {
java -jar "$rust_build_jar" \
BuildMetadataRustFromXml \
"$resources_dir/$1" \
"$generated_dir/$2.rs" \
"$3" \
"--const-name=$4"
}
# generate general metadata
generate "PhoneNumberMetadata.xml" "metadata" "metadata" "METADATA"
# generate short metadata
generate "PhoneNumberMetadataForTesting.xml" "test_metadata" "metadata" "TEST_METADATA"
echo "\
pub mod metadata;
pub mod test_metadata;
" > "$generated_dir/mod.rs"