Compare commits
35 Commits
e6c07d654f
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
519148ffd9 | ||
|
|
6be301ebd8 | ||
|
|
76a8d4857f | ||
|
|
7f7bab7f16 | ||
|
|
71d2562d83 | ||
|
|
9c67b42e9c | ||
|
|
56734bcb1c | ||
|
|
b64c063563 | ||
|
|
da9f5e9198 | ||
|
|
0ceb7c6c8c | ||
|
|
b979b290b8 | ||
|
|
923a941473 | ||
|
|
03911c0572 | ||
|
|
77fa0e2b09 | ||
|
|
f646fe4605 | ||
|
|
cb5f0d8fcc | ||
|
|
3a2e8e6c0f | ||
|
|
467416e3ef | ||
|
|
1464119ff8 | ||
|
|
beae04dee8 | ||
|
|
ebe7d236e9 | ||
|
|
2fea8f1e20 | ||
|
|
392c793d5c | ||
|
|
e7daffa6f7 | ||
|
|
10c5ee1159 | ||
|
|
8a42c0ecb5 | ||
|
|
e75eda86e6 | ||
|
|
1bb46ac1b7 | ||
|
|
0e683eac90 | ||
|
|
464711df8c | ||
|
|
e52a19e6c1 | ||
|
|
e72187d2d7 | ||
|
|
457bb65b9a | ||
|
|
3f07806990 | ||
|
|
4f5571f7a7 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
||||
/target
|
||||
.vscode
|
||||
872
Cargo.lock
generated
872
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
26
Cargo.toml
26
Cargo.toml
@@ -1,5 +1,5 @@
|
||||
[package]
|
||||
name = "rlibphonenumbers"
|
||||
name = "rlibphonenumber"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
@@ -10,7 +10,7 @@ build = "build/rust_build.rs"
|
||||
log = "0.4.27"
|
||||
# helpful error package
|
||||
thiserror = "2.0.12"
|
||||
# google protobuf lib required to use .proto files from assets
|
||||
# protobuf lib required to use .proto files from assets
|
||||
protobuf = "3.7.2"
|
||||
# optimized concurrent map
|
||||
dashmap = "6.1.0"
|
||||
@@ -21,11 +21,27 @@ 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"
|
||||
criterion = "0.5"
|
||||
phonenumber = "0.3"
|
||||
|
||||
[[bench]]
|
||||
name = "format_bench"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "parsing_bench"
|
||||
harness = false
|
||||
176
LICENSE
Normal file
176
LICENSE
Normal file
@@ -0,0 +1,176 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
121
Readme.md
Normal file
121
Readme.md
Normal file
@@ -0,0 +1,121 @@
|
||||
# libphonenumber-rust
|
||||
|
||||
[](https://crates.io/crates/rlibphonenumber)
|
||||
[](https://docs.rs/rlibphonenumber)
|
||||
[](https://opensource.org/licenses/Apache-2.0)
|
||||
|
||||
A Rust port of Google's comprehensive library for parsing, formatting, and validating international phone numbers.
|
||||
|
||||
## Overview
|
||||
|
||||
This library is a new adaptation of Google's `libphonenumber` for Rust. Its primary goal is to provide a powerful and efficient tool for handling phone numbers, with a structure that is intuitively close to the original C++ version.
|
||||
|
||||
You might be aware of an existing Rust implementation of `libphonenumber`. However, its maintenance has slowed, and I believe that a fresh start is the best path forward. This project aims to deliver a more direct and familiar port for developers acquainted with the C++ or Java versions of the original library.
|
||||
|
||||
This library gives you access to a wide range of functionalities, including:
|
||||
* Parsing and formatting phone numbers.
|
||||
* Validating phone numbers for all regions of the world.
|
||||
* Determining the number type (e.g., Mobile, Fixed-line, Toll-free).
|
||||
* Providing example numbers for every country.
|
||||
|
||||
## Performance
|
||||
|
||||
The following benchmarks were run against the `rust-phonenumber` crate. All tests were performed on the same machine and dataset. *Lower is better.*
|
||||
|
||||
### Formatting
|
||||
|
||||
| Format | rlibphonenumber (this crate) | rust-phonenumber | Performance Gain |
|
||||
|:---|:---:|:---:|:---:|
|
||||
| **E164** | **~78 ns** | ~2.59 µs | **~33x faster** |
|
||||
| **International** | **~1.34 µs** | ~3.21 µs | **~2.4x faster** |
|
||||
| **National** | **~2.33 µs** | ~4.87 µs | **~2.1x faster** |
|
||||
| **RFC3966** | **~1.62 µs** | ~3.47 µs | **~2.1x faster** |
|
||||
|
||||
### Parsing
|
||||
|
||||
| Task | rlibphonenumber (this crate) | rust-phonenumber | Performance Gain |
|
||||
|:--- |:---:|:---:|:---:|
|
||||
| **Parse** | **~11.60 µs** | ~13.45 µs | **~16% faster** |
|
||||
|
||||
This significant performance advantage is achieved through a focus on minimizing allocations, a more direct implementation path, and the use of modern tooling for metadata generation.
|
||||
|
||||
## Current Status
|
||||
|
||||
The project is currently in its initial phase of development. The core functionalities are being ported module by module to ensure quality and consistency.
|
||||
|
||||
### Implemented:
|
||||
* **PhoneNumberUtil:** The main utility for all phone number operations, such as parsing, formatting, and validation (Passes original tests).
|
||||
|
||||
### Future Plans:
|
||||
The roadmap includes porting the following key components:
|
||||
|
||||
* **AsYouTypeFormatter:** To format phone numbers as they are being typed.
|
||||
* **PhoneNumberOfflineGeocoder:** To provide geographical information for a phone number.
|
||||
* **PhoneNumberToCarrierMapper:** To identify the carrier associated with a phone number.
|
||||
|
||||
## Installation
|
||||
|
||||
Add this to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
rlibphonenumber = "0.1.0" # Replace with the actual version
|
||||
```
|
||||
|
||||
## Getting Started
|
||||
|
||||
Here is a basic example of how to parse and format a phone number:
|
||||
|
||||
```rust
|
||||
use rlibphonenumber::{PhoneNumberFormat, PHONE_NUMBER_UTIL};
|
||||
|
||||
fn main() {
|
||||
let number_to_parse = "+14155552671";
|
||||
let default_region = "US";
|
||||
|
||||
match PHONE_NUMBER_UTIL.parse(number_to_parse, default_region) {
|
||||
Ok(number) => {
|
||||
println!("Parsed number: {:?}", number);
|
||||
|
||||
let formatted_number = PHONE_NUMBER_UTIL.format(&number, PhoneNumberFormat::International).unwrap();
|
||||
println!("International format: {}", formatted_number);
|
||||
|
||||
let is_valid = PHONE_NUMBER_UTIL.is_valid_number(&number).unwrap();
|
||||
println!("Is the number valid? {}", is_valid);
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Error parsing number: {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## For Contributors
|
||||
|
||||
Contributions are **highly** welcome! Whether you are fixing a bug, improving documentation, or helping to port a new module, your help is appreciated.
|
||||
|
||||
### Code Generation
|
||||
|
||||
To maintain consistency with the original library, this project uses pre-compiled metadata. If you need to regenerate the metadata, for instance, after updating the `PhoneNumberMetadata.xml` file, you can use the provided tools.
|
||||
|
||||
The `tools` directory contains a rewritten Rust-based code generator for the C++ pre-compiled metadata.
|
||||
|
||||
To run the code generation process, simply execute the following script:
|
||||
|
||||
```sh
|
||||
./tools/scripts/generate_metadata.sh
|
||||
```
|
||||
|
||||
This script will:
|
||||
1. Build the Java-based tool that converts the XML metadata to a Rust-compatible format.
|
||||
2. Run the generator for the main metadata and the test metadata.
|
||||
3. Place the generated `.rs` files into the `src/generated/metadata` directory.
|
||||
|
||||
You can skip the Java build step by passing the `--skip-install` flag, which is useful if no changes were made to the generator itself.
|
||||
|
||||
```sh
|
||||
./tools/scripts/generate_metadata.sh --skip-install```
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the Apache License, Version 2.0. Please see the `LICENSE` file for details.
|
||||
76
benches/format_bench.rs
Normal file
76
benches/format_bench.rs
Normal file
@@ -0,0 +1,76 @@
|
||||
use criterion::{Criterion, black_box, criterion_group, criterion_main};
|
||||
|
||||
use rlibphonenumber::{PhoneNumberFormat, PHONE_NUMBER_UTIL};
|
||||
|
||||
use phonenumber::{
|
||||
self as rlp,
|
||||
country::Id::{self, AU}, Mode,
|
||||
};
|
||||
|
||||
type TestEntity = (&'static str, &'static str, Id);
|
||||
|
||||
fn setup_numbers() -> Vec<TestEntity> {
|
||||
vec![("0011 54 9 11 8765 4321 ext. 1234", "AU", AU)]
|
||||
}
|
||||
|
||||
fn convert_to_rlp_numbers(numbers: &[TestEntity]) -> Vec<rlp::PhoneNumber> {
|
||||
numbers
|
||||
.iter()
|
||||
.map(|s| rlp::parse(Some(s.2), s.0).unwrap())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn convert_to_rlibphonenumber_numbers(
|
||||
numbers: &[TestEntity],
|
||||
) -> Vec<rlibphonenumber::PhoneNumber> {
|
||||
numbers
|
||||
.iter()
|
||||
.map(|s| PHONE_NUMBER_UTIL.parse(s.0, s.1).unwrap())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn formatting_benchmark(c: &mut Criterion) {
|
||||
let numbers = setup_numbers();
|
||||
let rlp_numbers = convert_to_rlp_numbers(&numbers);
|
||||
let numbers = convert_to_rlibphonenumber_numbers(&numbers);
|
||||
|
||||
let mut group = c.benchmark_group("Formatting Comparison");
|
||||
|
||||
let mut test = |format_a: PhoneNumberFormat, format_b: Mode| {
|
||||
group.bench_function(format!("rlibphonenumber: format({:?})", format_a), |b| {
|
||||
b.iter(|| {
|
||||
for number in &numbers {
|
||||
PHONE_NUMBER_UTIL
|
||||
.format(black_box(number), black_box(format_a))
|
||||
.unwrap();
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
group.bench_function(format!("rust-phonenumber: format({:?})", format_b), |b| {
|
||||
b.iter(|| {
|
||||
for number in &rlp_numbers {
|
||||
rlp::format(black_box(number)).mode(format_b).to_string();
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
for (number_a, number_b) in rlp_numbers.iter().zip(numbers.iter()) {
|
||||
assert_eq!(
|
||||
rlp::format(number_a).mode(format_b).to_string(),
|
||||
PHONE_NUMBER_UTIL
|
||||
.format(number_b, format_a)
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
test(PhoneNumberFormat::E164, Mode::E164);
|
||||
test(PhoneNumberFormat::International, Mode::International);
|
||||
test(PhoneNumberFormat::National, Mode::National);
|
||||
test(PhoneNumberFormat::RFC3966, Mode::Rfc3966);
|
||||
group.finish();
|
||||
}
|
||||
|
||||
criterion_group!(benches, formatting_benchmark);
|
||||
criterion_main!(benches);
|
||||
73
benches/parsing_bench.rs
Normal file
73
benches/parsing_bench.rs
Normal file
@@ -0,0 +1,73 @@
|
||||
// benches/parsing_benchmark.rs
|
||||
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
|
||||
// --- Импорты из вашей библиотеки ---
|
||||
use rlibphonenumber::PHONE_NUMBER_UTIL;
|
||||
|
||||
// --- Импорты из внешней библиотеки ---
|
||||
use phonenumber::{self as rlp, country::Id};
|
||||
|
||||
// Тип для наших тестовых данных: (строка_номера, регион_для_вас, регион_для_rlp)
|
||||
type TestEntity = (&'static str, &'static str, Id);
|
||||
|
||||
/// Подготавливает разнообразный набор данных для тестирования парсинга.
|
||||
/// Это дает более объективную оценку, чем один номер.
|
||||
fn setup_parsing_data() -> Vec<TestEntity> {
|
||||
use phonenumber::country::Id::*;
|
||||
vec![
|
||||
// Оригинальный номер из вашего примера
|
||||
("0011 54 9 11 8765 4321 ext. 1234", "AU", AU),
|
||||
// Простой номер США в национальном формате
|
||||
("(650) 253-0000", "US", US),
|
||||
// Номер Великобритании в международном формате со знаком +
|
||||
("+44 20 8765 4321", "GB", GB),
|
||||
// Номер Великобритании с национальным префиксом (ведущий ноль)
|
||||
("020 8765 4321", "GB", GB),
|
||||
// Сложный мобильный номер Аргентины
|
||||
("011 15-1234-5678", "AR", AR),
|
||||
// Итальянский номер со значащим ведущим нулем
|
||||
("02 12345678", "IT", IT),
|
||||
// "Vanity" номер (с буквами)
|
||||
("1-800-FLOWERS", "US", US),
|
||||
// Короткий номер, который может быть валидным в некоторых регионах
|
||||
("12345", "DE", DE),
|
||||
]
|
||||
}
|
||||
|
||||
fn parsing_benchmark(c: &mut Criterion) {
|
||||
// Получаем наш набор тестовых данных
|
||||
let numbers_to_parse = setup_parsing_data();
|
||||
|
||||
let mut group = c.benchmark_group("Parsing Comparison");
|
||||
|
||||
// --- Бенчмарк для вашей библиотеки rlibphonenumber ---
|
||||
group.bench_function("rlibphonenumber: parse()", |b| {
|
||||
// b.iter() запускает код в цикле много раз для замера
|
||||
b.iter(|| {
|
||||
// Итерируемся по всем номерам в нашем наборе
|
||||
for (number_str, region, _) in &numbers_to_parse {
|
||||
// Вызываем parse, обернув аргументы в black_box.
|
||||
// Это гарантирует, что компилятор не оптимизирует вызов.
|
||||
// Мы не используем результат, так как нас интересует только скорость выполнения.
|
||||
let _ = PHONE_NUMBER_UTIL.parse(black_box(number_str), black_box(region));
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
// --- Бенчмарк для библиотеки rust-phonenumber ---
|
||||
group.bench_function("rust-phonenumber: parse()", |b| {
|
||||
b.iter(|| {
|
||||
for (number_str, _, region_id) in &numbers_to_parse {
|
||||
// Аналогичный вызов для второй библиотеки
|
||||
let _ = rlp::parse(black_box(Some(*region_id)), black_box(number_str));
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
// Макросы для регистрации и запуска бенчмарка
|
||||
criterion_group!(benches, parsing_benchmark);
|
||||
criterion_main!(benches);
|
||||
@@ -1,76 +1,20 @@
|
||||
/**
|
||||
* This file represents content of https://github.com/google/libphonenumber/tree/master/tools/cpp
|
||||
*/
|
||||
|
||||
use std::{collections::BTreeMap, fs::File, io::{BufRead, BufReader}, num::ParseIntError, path::Path};
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
enum BuildError {
|
||||
#[error("IO error occurred: {0}")]
|
||||
IO(#[from] std::io::Error),
|
||||
|
||||
#[error("Line {line_num} is too long (max is {max_len} bytes)")]
|
||||
LineTooLong { line_num: usize, max_len: usize },
|
||||
|
||||
#[error("Failed to parse prefix '{prefix}': {source}")]
|
||||
PrefixParseError {
|
||||
prefix: String,
|
||||
#[source]
|
||||
source: ParseIntError,
|
||||
},
|
||||
}
|
||||
|
||||
fn parse_prefixes(path: &str, prefixes: &mut BTreeMap<i32, String>) -> Result<(), BuildError> {
|
||||
prefixes.clear();
|
||||
|
||||
let input = File::open(path)?;
|
||||
const MAX_LINE_LENGTH: usize = 2 * 1024;
|
||||
|
||||
let mut reader = BufReader::new(input);
|
||||
let mut line_buffer = String::with_capacity(MAX_LINE_LENGTH);
|
||||
let mut line_number = 0;
|
||||
|
||||
loop {
|
||||
line_number += 1;
|
||||
line_buffer.clear();
|
||||
|
||||
let bytes_read = reader.read_line(&mut line_buffer)?;
|
||||
if bytes_read == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
if !line_buffer.ends_with('\n') {
|
||||
return Err(BuildError::LineTooLong {
|
||||
line_num: line_number,
|
||||
max_len: MAX_LINE_LENGTH,
|
||||
});
|
||||
}
|
||||
|
||||
let line = line_buffer.trim();
|
||||
if line.is_empty() || line.starts_with('#') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some((prefix_str, desc)) = line.split_once('|') {
|
||||
if prefix_str.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let prefix_code = prefix_str.parse().map_err(|e| BuildError::PrefixParseError {
|
||||
prefix: prefix_str.to_string(),
|
||||
source: e,
|
||||
})?;
|
||||
prefixes.insert(prefix_code, desc.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
// Copyright (C) 2009 The Libphonenumber Authors
|
||||
// Copyright (C) 2025 The Kashin Vladislav (Rust adaptation author)
|
||||
//
|
||||
// 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.
|
||||
|
||||
|
||||
|
||||
fn main() -> Result<(), BuildError> {
|
||||
fn main() {
|
||||
protobuf_codegen::Codegen::new()
|
||||
.pure()
|
||||
.includes(["resources"])
|
||||
@@ -78,5 +22,4 @@ fn main() -> Result<(), BuildError> {
|
||||
.input("resources/phonenumber.proto")
|
||||
.cargo_out_dir("proto_gen")
|
||||
.run_from_script();
|
||||
Ok(())
|
||||
}
|
||||
@@ -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
|
||||
];
|
||||
|
||||
26
src/generated/metadata/mod.rs
Normal file
26
src/generated/metadata/mod.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
// Copyright (C) 2009 The Libphonenumber Authors
|
||||
// Copyright (C) 2025 The Kashin Vladislav (Rust adaptation author)
|
||||
//
|
||||
// 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.
|
||||
|
||||
|
||||
mod metadata;
|
||||
|
||||
// use only in test case
|
||||
#[cfg(test)]
|
||||
mod test_metadata;
|
||||
|
||||
pub use metadata::METADATA;
|
||||
#[cfg(test)]
|
||||
pub use test_metadata::TEST_METADATA;
|
||||
|
||||
1039
src/generated/metadata/test_metadata.rs
Normal file
1039
src/generated/metadata/test_metadata.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,5 @@
|
||||
// Copyright (C) 2025 @Vloldik
|
||||
// Copyright (C) 2009 The Libphonenumber Authors
|
||||
// Copyright (C) 2025 The Kashin Vladislav (Rust adaptation author)
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -11,5 +12,6 @@
|
||||
// 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.
|
||||
//
|
||||
// This file is generated automatically, do not edit it manually.
|
||||
|
||||
pub mod proto;
|
||||
pub mod metadata;
|
||||
@@ -1,3 +0,0 @@
|
||||
mod region_code;
|
||||
|
||||
pub use region_code::RegionCode;
|
||||
@@ -1,13 +0,0 @@
|
||||
pub struct RegionCode {
|
||||
}
|
||||
|
||||
impl RegionCode {
|
||||
/// Returns a region code string representing the "unknown" region.
|
||||
pub fn get_unknown() -> &'static str {
|
||||
return Self::zz();
|
||||
}
|
||||
|
||||
pub fn zz() -> &'static str {
|
||||
return "ZZ";
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,23 @@
|
||||
// Copyright (C) 2009 The Libphonenumber Authors
|
||||
// Copyright (C) 2025 The Kashin Vladislav (Rust adaptation author)
|
||||
//
|
||||
// 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.
|
||||
|
||||
use crate::generated::proto::phonemetadata::PhoneNumberDesc;
|
||||
|
||||
use crate::proto_gen::phonemetadata::PhoneNumberDesc;
|
||||
/// Internal phonenumber matching API used to isolate the underlying
|
||||
/// implementation of the matcher and allow different implementations to be
|
||||
/// swapped in easily.
|
||||
|
||||
pub(crate) trait MatcherApi: Send + Sync {
|
||||
/// Returns whether the given national number (a string containing only decimal
|
||||
/// digits) matches the national number pattern defined in the given
|
||||
|
||||
44
src/lib.rs
44
src/lib.rs
@@ -1,11 +1,25 @@
|
||||
mod shortnumberinfo;
|
||||
// Copyright (C) 2009 The Libphonenumber Authors
|
||||
// Copyright (C) 2025 The Kashin Vladislav (Rust adaptation author)
|
||||
//
|
||||
// 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.
|
||||
|
||||
mod interfaces;
|
||||
/// This module is automatically generated from /resources/*.proto
|
||||
mod proto_gen;
|
||||
mod generated;
|
||||
mod phonenumberutil;
|
||||
mod regexp_cache;
|
||||
mod regex_based_matcher;
|
||||
pub mod i18n;
|
||||
pub mod region_code;
|
||||
pub(crate) mod regex_util;
|
||||
pub(crate) mod string_util;
|
||||
|
||||
@@ -13,4 +27,26 @@ 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 generated::proto::phonemetadata;
|
||||
pub use generated::proto::phonenumber::PhoneNumber;
|
||||
pub use generated::proto::phonenumber::phone_number::CountryCodeSource;
|
||||
pub use regexp_cache::InvalidRegexError;
|
||||
mod tests;
|
||||
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// Copyright (C) 2025 The Kashin Vladislav (Rust adaptation author)
|
||||
//
|
||||
// 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.
|
||||
|
||||
// std::borrow::Cow
|
||||
// std::option::Option
|
||||
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
// Copyright (C) 2009 The Libphonenumber Authors
|
||||
// Copyright (C) 2025 The Kashin Vladislav (Rust adaptation author)
|
||||
//
|
||||
// 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.
|
||||
|
||||
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
|
||||
@@ -64,35 +78,9 @@ pub enum MatchType {
|
||||
// Separated enum ValidationResult into ValidationResult err and
|
||||
// ValidationResultOk for using Result<Ok, Err>
|
||||
|
||||
/// Possible outcomes when testing if a PhoneNumber is possible.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Error)]
|
||||
pub enum ValidationResultErr {
|
||||
/// The number has an invalid country calling code.
|
||||
#[error("The number has an invalid country calling code")]
|
||||
InvalidCountryCode,
|
||||
/// The number is shorter than all valid numbers for this region.
|
||||
#[error("The number is shorter than all valid numbers for this region")]
|
||||
TooShort,
|
||||
/// The number is longer than the shortest valid numbers for this region,
|
||||
/// shorter than the longest valid numbers for this region, and does not
|
||||
/// itself have a number length that matches valid numbers for this region.
|
||||
/// This can also be returned in the case where
|
||||
/// IsPossibleNumberForTypeWithReason was called, and there are no numbers of
|
||||
/// this type at all for this region.
|
||||
#[error("\
|
||||
The number is longer than the shortest valid numbers for this region,\
|
||||
shorter than the longest valid numbers for this region, and does not\
|
||||
itself have a number length that matches valid numbers for this region\
|
||||
")]
|
||||
InvalidLength,
|
||||
/// The number is longer than all valid numbers for this region.
|
||||
#[error("The number is longer than all valid numbers for this region")]
|
||||
TooLong,
|
||||
}
|
||||
|
||||
/// Possible outcomes when testing if a PhoneNumber is possible.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum ValidNumberLenType {
|
||||
pub enum NumberLengthType {
|
||||
/// The number length matches that of valid numbers for this region.
|
||||
IsPossible,
|
||||
/// The number length matches that of local numbers for this region only
|
||||
|
||||
@@ -1,18 +1,31 @@
|
||||
use core::error;
|
||||
// Copyright (C) 2009 The Libphonenumber Authors
|
||||
// Copyright (C) 2025 The Kashin Vladislav (Rust adaptation author)
|
||||
//
|
||||
// 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.
|
||||
|
||||
use std::num::ParseIntError;
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::regexp_cache::ErrorInvalidRegex;
|
||||
use crate::regexp_cache::InvalidRegexError;
|
||||
|
||||
#[derive(Debug, PartialEq, Error)]
|
||||
pub enum PhoneNumberUtilError {
|
||||
pub enum InternalLogicError {
|
||||
#[error("{0}")]
|
||||
InvalidRegexError(#[from] ErrorInvalidRegex),
|
||||
#[error("Parse error: {0}")]
|
||||
ParseError(#[from] ParseError),
|
||||
#[error("Extract number error: {0}")]
|
||||
ExtractNumberError(#[from] ExtractNumberError)
|
||||
InvalidRegex(#[from] InvalidRegexError),
|
||||
|
||||
#[error("{0}")]
|
||||
InvalidMetadataForValidRegion(#[from] InvalidMetadataForValidRegionError)
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Error)]
|
||||
@@ -20,9 +33,9 @@ pub enum ParseError {
|
||||
// Removed as OK variant
|
||||
// NoParsingError,
|
||||
#[error("Invalid country code")]
|
||||
InvalidCountryCodeError, // INVALID_COUNTRY_CODE in the java version.
|
||||
#[error("Not a number")]
|
||||
NotANumber,
|
||||
InvalidCountryCode, // INVALID_COUNTRY_CODE in the java version.
|
||||
#[error("Not a number: {0}")]
|
||||
NotANumber(#[from] NotANumberError),
|
||||
#[error("Too short after idd")]
|
||||
TooShortAfterIdd,
|
||||
#[error("Too short Nsn")]
|
||||
@@ -30,11 +43,19 @@ pub enum ParseError {
|
||||
#[error("Too long nsn")]
|
||||
TooLongNsn, // TOO_LONG in the java version.
|
||||
#[error("{0}")]
|
||||
InvalidRegexError(#[from] ErrorInvalidRegex),
|
||||
InvalidRegex(#[from] InvalidRegexError),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Error)]
|
||||
pub enum NotANumberError {
|
||||
#[error("Number not matched a valid number pattern")]
|
||||
NotMatchedValidNumberPattern,
|
||||
#[error("Invalid phone context")]
|
||||
InvalidPhoneContext,
|
||||
#[error("{0}")]
|
||||
ParseNumberAsIntError(#[from] ParseIntError),
|
||||
FailedToParseNumberAsInt(#[from] ParseIntError),
|
||||
#[error("{0}")]
|
||||
ExtractNumberError(#[from] ExtractNumberError),
|
||||
FailedToExtractNumber(#[from] ExtractNumberError),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Error)]
|
||||
@@ -45,23 +66,57 @@ pub enum ExtractNumberError {
|
||||
NotANumber,
|
||||
}
|
||||
|
||||
impl From<ExtractNumberError> for ParseError {
|
||||
fn from(value: ExtractNumberError) -> Self {
|
||||
NotANumberError::FailedToExtractNumber(value).into()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Error)]
|
||||
pub enum GetExampleNumberError {
|
||||
#[error("Parse error: {0}")]
|
||||
ParseError(#[from] ParseError),
|
||||
FailedToParse(#[from] ParseError),
|
||||
#[error("{0}")]
|
||||
InvalidRegexError(#[from] ErrorInvalidRegex),
|
||||
Internal(#[from] InternalLogicError),
|
||||
#[error("No example number")]
|
||||
NoExampleNumberError,
|
||||
NoExampleNumber,
|
||||
#[error("Could not get number")]
|
||||
CouldNotGetNumberError,
|
||||
CouldNotGetNumber,
|
||||
#[error("Invalid metadata")]
|
||||
InvalidMetadataError
|
||||
InvalidMetadata
|
||||
}
|
||||
|
||||
|
||||
#[derive(Error, Debug, PartialEq)]
|
||||
pub enum MatchError {
|
||||
#[error("Invalid number given")]
|
||||
InvalidNumber(#[from] ParseError), // NOT_A_NUMBER in the java version.
|
||||
#[error("Invalid number given")]
|
||||
pub struct InvalidNumberError(#[from] pub ParseError); // NOT_A_NUMBER in the java version
|
||||
|
||||
#[derive(Debug, Error, PartialEq)]
|
||||
#[error("Metadata for valid region MUST not be null")]
|
||||
pub struct InvalidMetadataForValidRegionError;
|
||||
|
||||
/// Possible outcomes when testing if a PhoneNumber is possible.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Error)]
|
||||
pub enum ValidationError {
|
||||
/// The number has an invalid country calling code.
|
||||
#[error("The number has an invalid country calling code")]
|
||||
InvalidCountryCode,
|
||||
/// The number is shorter than all valid numbers for this region.
|
||||
#[error("The number is shorter than all valid numbers for this region")]
|
||||
TooShort,
|
||||
/// The number is longer than the shortest valid numbers for this region,
|
||||
/// shorter than the longest valid numbers for this region, and does not
|
||||
/// itself have a number length that matches valid numbers for this region.
|
||||
/// This can also be returned in the case where
|
||||
/// IsPossibleNumberForTypeWithReason was called, and there are no numbers of
|
||||
/// this type at all for this region.
|
||||
#[error("\
|
||||
The number is longer than the shortest valid numbers for this region,\
|
||||
shorter than the longest valid numbers for this region, and does not\
|
||||
itself have a number length that matches valid numbers for this region\
|
||||
")]
|
||||
InvalidLength,
|
||||
/// The number is longer than all valid numbers for this region.
|
||||
#[error("The number is longer than all valid numbers for this region")]
|
||||
TooLong,
|
||||
}
|
||||
@@ -1,3 +1,19 @@
|
||||
// Copyright (C) 2009 The Libphonenumber Authors
|
||||
// Copyright (C) 2025 The Kashin Vladislav (Rust adaptation author)
|
||||
//
|
||||
// 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.
|
||||
|
||||
|
||||
// The minimum and maximum length of the national significant number.
|
||||
pub const MIN_LENGTH_FOR_NSN: usize = 2;
|
||||
// The ITU says the maximum length should be 15, but we have found longer
|
||||
@@ -15,7 +31,7 @@ pub const PLUS_CHARS: &'static str = "+\u{FF0B}";
|
||||
pub const VALID_PUNCTUATION: &'static str = "-x\
|
||||
\u{2010}-\u{2015}\u{2212}\u{30FC}\u{FF0D}-\u{FF0F} \u{00A0}\
|
||||
\u{00AD}\u{200B}\u{2060}\u{3000}()\u{FF08}\u{FF09}\u{FF3B}\
|
||||
\u{FF3D}.[]/~\u{2053}\u{223C}";
|
||||
\u{FF3D}.\\[\\]/~\u{2053}\u{223C}";
|
||||
|
||||
// Regular expression of characters typically used to start a second phone
|
||||
// number for the purposes of parsing. This allows us to strip off parts of
|
||||
@@ -28,7 +44,7 @@ pub const VALID_PUNCTUATION: &'static str = "-x\
|
||||
pub const CAPTURE_UP_TO_SECOND_NUMBER_START: &'static str = r"(.*)[\\/] *x";
|
||||
|
||||
|
||||
pub const REGION_CODE_FOR_NON_GEO_ENTITY: &'static str = "0001";
|
||||
pub const REGION_CODE_FOR_NON_GEO_ENTITY: &'static str = "001";
|
||||
|
||||
pub const PLUS_SIGN: &'static str = "+";
|
||||
pub const STAR_SIGN: &'static str = "*";
|
||||
@@ -50,11 +66,11 @@ pub const VALID_ALPHA_INCL_UPPERCASE: &'static str = "A-Za-z";
|
||||
// prefix. This can be overridden by region-specific preferences.
|
||||
pub const DEFAULT_EXTN_PREFIX: &'static str = " ext. ";
|
||||
|
||||
pub const POSSIBLE_SEPARATORS_BETWEEN_NUMBER_AND_EXT_LABEL: &'static str = "0001";
|
||||
pub const POSSIBLE_SEPARATORS_BETWEEN_NUMBER_AND_EXT_LABEL: &'static str = "[ \u{00A0}\\t,]*";
|
||||
|
||||
// Optional full stop (.) or colon, followed by zero or more
|
||||
// spaces/tabs/commas.
|
||||
pub const POSSIBLE_CHARS_AFTER_EXT_LABEL: &'static str = "[ \u{00A0}\\t,]*";
|
||||
pub const OPTIONAL_EXT_SUFFIX: &'static str = "[:\\.\u{FF0E}]?[ \u{00A0}\\t,-]*";
|
||||
pub const POSSIBLE_CHARS_AFTER_EXT_LABEL: &'static str = "[:\\.\u{FF0E}]?[ \u{00A0}\\t,-]*";
|
||||
pub const OPTIONAL_EXT_SUFFIX: &'static str = "#?";
|
||||
|
||||
pub const NANPA_COUNTRY_CODE: i32 = 1;
|
||||
@@ -1,5 +0,0 @@
|
||||
mod helper_constants;
|
||||
mod metadata;
|
||||
|
||||
pub(super) use helper_constants::{*};
|
||||
pub(super) use metadata::METADATA;
|
||||
@@ -1,33 +1,51 @@
|
||||
// Copyright (C) 2009 The Libphonenumber Authors
|
||||
// Copyright (C) 2025 The Kashin Vladislav (Rust adaptation author)
|
||||
//
|
||||
// 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.
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use protobuf::Message;
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
use crate::{
|
||||
interfaces::MatcherApi,
|
||||
proto_gen::{
|
||||
phonemetadata::{PhoneMetadata, PhoneMetadataCollection, PhoneNumberDesc},
|
||||
interfaces::MatcherApi, generated::metadata::METADATA,
|
||||
generated::proto::{
|
||||
phonemetadata::{
|
||||
PhoneMetadata, PhoneMetadataCollection, PhoneNumberDesc
|
||||
},
|
||||
phonenumber::PhoneNumber,
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
use super::{
|
||||
PhoneNumberFormat, PhoneNumberType, ValidNumberLenType, ValidationResultErr,
|
||||
enums::{PhoneNumberFormat, PhoneNumberType, NumberLengthType},
|
||||
errors::ValidationError,
|
||||
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,
|
||||
},
|
||||
};
|
||||
|
||||
/// Loads metadata from helper constants METADATA array
|
||||
pub(super) fn load_compiled_metadata() -> Result<PhoneMetadataCollection, protobuf::Error> {
|
||||
pub fn load_compiled_metadata() -> Result<PhoneMetadataCollection, protobuf::Error> {
|
||||
let result = PhoneMetadataCollection::parse_from_bytes(&METADATA)?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Returns a pointer to the description inside the metadata of the appropriate
|
||||
/// type.
|
||||
pub(super) fn get_number_desc_by_type(
|
||||
pub fn get_number_desc_by_type(
|
||||
metadata: &PhoneMetadata,
|
||||
phone_number_type: PhoneNumberType,
|
||||
) -> &PhoneNumberDesc {
|
||||
@@ -48,7 +66,7 @@ pub(super) fn get_number_desc_by_type(
|
||||
}
|
||||
|
||||
/// A helper function that is used by Format and FormatByPattern.
|
||||
pub(super) fn prefix_number_with_country_calling_code(
|
||||
pub fn prefix_number_with_country_calling_code(
|
||||
country_calling_code: i32,
|
||||
number_format: PhoneNumberFormat,
|
||||
formatted_number: &mut String,
|
||||
@@ -91,7 +109,7 @@ pub(super) fn prefix_number_with_country_calling_code(
|
||||
|
||||
// Returns true when one national number is the suffix of the other or both are
|
||||
// the same.
|
||||
pub(super) fn is_national_number_suffix_of_the_other(
|
||||
pub fn is_national_number_suffix_of_the_other(
|
||||
first_number: &PhoneNumber,
|
||||
second_number: &PhoneNumber,
|
||||
) -> bool {
|
||||
@@ -106,7 +124,7 @@ pub(super) fn is_national_number_suffix_of_the_other(
|
||||
|
||||
/// Helper method for constructing regular expressions for parsing. Creates an
|
||||
/// expression that captures up to max_length digits.
|
||||
pub(super) fn extn_digits(max_length: u32) -> String {
|
||||
pub fn extn_digits(max_length: u32) -> String {
|
||||
let mut buf = itoa::Buffer::new();
|
||||
let max_length_str = buf.format(max_length);
|
||||
const HELPER_STR_LEN: usize = 2 + 4 + 2;
|
||||
@@ -131,7 +149,7 @@ pub(super) fn extn_digits(max_length: u32) -> String {
|
||||
// number is changed, MaybeStripExtension needs to be updated.
|
||||
// - The only capturing groups should be around the digits that you want to
|
||||
// capture as part of the extension, or else parsing will fail!
|
||||
pub(super) fn create_extn_pattern(for_parsing: bool) -> String {
|
||||
pub fn create_extn_pattern(for_parsing: bool) -> String {
|
||||
// We cap the maximum length of an extension based on the ambiguity of the
|
||||
// way the extension is prefixed. As per ITU, the officially allowed
|
||||
// length for extensions is actually 40, but we don't support this since we
|
||||
@@ -255,7 +273,7 @@ pub(super) fn create_extn_pattern(for_parsing: bool) -> String {
|
||||
/// left unchanged in the number.
|
||||
///
|
||||
/// Returns: normalized_string
|
||||
pub(super) fn normalize_helper(
|
||||
pub fn normalize_helper(
|
||||
normalization_replacements: &HashMap<char, char>,
|
||||
remove_non_matches: bool,
|
||||
phone_number: &str
|
||||
@@ -276,7 +294,7 @@ pub(super) fn normalize_helper(
|
||||
|
||||
/// Returns `true` if there is any possible number data set for a particular
|
||||
/// PhoneNumberDesc.
|
||||
pub(super) fn desc_has_possible_number_data(desc: &PhoneNumberDesc) -> bool {
|
||||
pub fn desc_has_possible_number_data(desc: &PhoneNumberDesc) -> bool {
|
||||
// If this is empty, it means numbers of this type inherit from the "general
|
||||
// desc" -> the value "-1" means that no numbers exist for this type.
|
||||
return desc.possible_length.len() != 1
|
||||
@@ -296,7 +314,7 @@ pub(super) fn desc_has_possible_number_data(desc: &PhoneNumberDesc) -> bool {
|
||||
/// mention why during a review without needing to change MetadataFilter.
|
||||
///
|
||||
/// Returns `true` if there is any data set for a particular PhoneNumberDesc.
|
||||
pub(super) fn desc_has_data(desc: &PhoneNumberDesc) -> bool {
|
||||
pub fn desc_has_data(desc: &PhoneNumberDesc) -> bool {
|
||||
// Checking most properties since we don't know what's present, since a custom
|
||||
// build may have stripped just one of them (e.g. USE_METADATA_LITE strips
|
||||
// exampleNumber). We don't bother checking the PossibleLengthsLocalOnly,
|
||||
@@ -309,7 +327,7 @@ pub(super) fn desc_has_data(desc: &PhoneNumberDesc) -> bool {
|
||||
|
||||
/// Returns the types we have metadata for based on the PhoneMetadata object
|
||||
/// passed in.
|
||||
pub(super) fn populate_supported_types_for_metadata(
|
||||
pub fn populate_supported_types_for_metadata(
|
||||
metadata: &PhoneMetadata,
|
||||
types: &mut HashSet<PhoneNumberType>,
|
||||
) {
|
||||
@@ -329,7 +347,7 @@ pub(super) fn populate_supported_types_for_metadata(
|
||||
});
|
||||
}
|
||||
|
||||
pub(super) fn get_supported_types_for_metadata(metadata: &PhoneMetadata) -> HashSet<PhoneNumberType> {
|
||||
pub fn get_supported_types_for_metadata(metadata: &PhoneMetadata) -> HashSet<PhoneNumberType> {
|
||||
const EFFECTIVE_NUMBER_TYPES: usize = 11 /* count */ - 2 /* filter type or unknown */;
|
||||
let mut types = HashSet::with_capacity(EFFECTIVE_NUMBER_TYPES);
|
||||
populate_supported_types_for_metadata(metadata, &mut types);
|
||||
@@ -338,11 +356,11 @@ pub(super) fn get_supported_types_for_metadata(metadata: &PhoneMetadata) -> Hash
|
||||
|
||||
/// Helper method to check a number against possible lengths for this number
|
||||
/// type, and determine whether it matches, or is too short or too long.
|
||||
pub(super) fn test_number_length(
|
||||
pub fn test_number_length(
|
||||
phone_number: &str,
|
||||
phone_metadata: &PhoneMetadata,
|
||||
phone_number_type: PhoneNumberType,
|
||||
) -> Result<ValidNumberLenType, ValidationResultErr> {
|
||||
) -> Result<NumberLengthType, ValidationError> {
|
||||
let desc_for_type = get_number_desc_by_type(phone_metadata, phone_number_type);
|
||||
// There should always be "possibleLengths" set for every element. This is
|
||||
// declared in the XML schema which is verified by
|
||||
@@ -394,41 +412,41 @@ pub(super) fn test_number_length(
|
||||
// If the type is not suported at all (indicated by the possible lengths
|
||||
// containing -1 at this point) we return invalid length.
|
||||
if *possible_lengths.first().unwrap_or(&-1) == -1 {
|
||||
return Err(ValidationResultErr::InvalidLength);
|
||||
return Err(ValidationError::InvalidLength);
|
||||
}
|
||||
|
||||
let actual_length = phone_number.len() as i32;
|
||||
// This is safe because there is never an overlap beween the possible lengths
|
||||
// and the local-only lengths; this is checked at build time.
|
||||
if local_lengths.contains(&actual_length) {
|
||||
return Ok(ValidNumberLenType::IsPossibleLocalOnly);
|
||||
return Ok(NumberLengthType::IsPossibleLocalOnly);
|
||||
}
|
||||
|
||||
// here we can unwrap safe
|
||||
let minimum_length = possible_lengths[0];
|
||||
|
||||
if minimum_length == actual_length {
|
||||
return Ok(ValidNumberLenType::IsPossible);
|
||||
return Ok(NumberLengthType::IsPossible);
|
||||
} else if minimum_length > actual_length {
|
||||
return Err(ValidationResultErr::TooShort);
|
||||
return Err(ValidationError::TooShort);
|
||||
} else if possible_lengths[possible_lengths.len() - 1] < actual_length {
|
||||
return Err(ValidationResultErr::TooLong);
|
||||
return Err(ValidationError::TooLong);
|
||||
}
|
||||
// We skip the first element; we've already checked it.
|
||||
return if possible_lengths[1..].contains(&actual_length) {
|
||||
Ok(ValidNumberLenType::IsPossible)
|
||||
Ok(NumberLengthType::IsPossible)
|
||||
} else {
|
||||
Err(ValidationResultErr::InvalidLength)
|
||||
Err(ValidationError::InvalidLength)
|
||||
};
|
||||
}
|
||||
|
||||
/// Helper method to check a number against possible lengths for this region,
|
||||
/// based on the metadata being passed in, and determine whether it matches, or
|
||||
/// is too short or too long.
|
||||
pub(super) fn test_number_length_with_unknown_type(
|
||||
pub fn test_number_length_with_unknown_type(
|
||||
phone_number: &str,
|
||||
phone_metadata: &PhoneMetadata,
|
||||
) -> Result<ValidNumberLenType, ValidationResultErr> {
|
||||
) -> Result<NumberLengthType, ValidationError> {
|
||||
return test_number_length(phone_number, phone_metadata, PhoneNumberType::Unknown);
|
||||
}
|
||||
|
||||
@@ -454,7 +472,7 @@ pub(crate) fn copy_core_fields_only(from_number: &PhoneNumber) -> PhoneNumber {
|
||||
|
||||
/// Determines whether the given number is a national number match for the given
|
||||
/// PhoneNumberDesc. Does not check against possible lengths!
|
||||
pub(super) fn is_match(
|
||||
pub fn is_match(
|
||||
matcher_api: &Box<dyn MatcherApi>,
|
||||
number: &str,
|
||||
number_desc: &PhoneNumberDesc,
|
||||
|
||||
@@ -1,6 +1,22 @@
|
||||
// Copyright (C) 2009 The Libphonenumber Authors
|
||||
// Copyright (C) 2025 The Kashin Vladislav (Rust adaptation author)
|
||||
//
|
||||
// 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.
|
||||
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use crate::proto_gen::phonenumber::phone_number::CountryCodeSource;
|
||||
use crate::CountryCodeSource;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PhoneNumberWithCountryCodeSource<'a> {
|
||||
@@ -13,19 +29,3 @@ impl<'a> PhoneNumberWithCountryCodeSource<'a> {
|
||||
Self { phone_number, country_code_source }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PhoneNumberAndCarrierCode<'a> {
|
||||
pub carrier_code: Option<&'a str>,
|
||||
pub phone_number: Cow<'a, str>
|
||||
}
|
||||
|
||||
impl<'a> PhoneNumberAndCarrierCode<'a> {
|
||||
pub fn new<B: Into<Cow<'a, str>>>(carrier_code: Option<&'a str>, phone_number: B) -> Self {
|
||||
Self { carrier_code, phone_number: phone_number.into() }
|
||||
}
|
||||
|
||||
pub fn new_phone<B: Into<Cow<'a, str>>>(phone_number: B) -> Self {
|
||||
Self { carrier_code: None, phone_number: phone_number.into() }
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,31 @@
|
||||
// Copyright (C) 2009 The Libphonenumber Authors
|
||||
// Copyright (C) 2025 The Kashin Vladislav (Rust adaptation author)
|
||||
//
|
||||
// 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.
|
||||
|
||||
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;
|
||||
|
||||
use std::sync::LazyLock;
|
||||
|
||||
pub use enums::{MatchType, PhoneNumberFormat, PhoneNumberType, ValidationResultErr, ValidNumberLenType};
|
||||
use thiserror::Error;
|
||||
use crate::phonenumberutil::phonenumberutil::PhoneNumberUtil;
|
||||
|
||||
// use crate::phonenumberutil::phonenumberutil::PhoneNumberUtil;
|
||||
|
||||
// static PHONE_NUMBER_UTIL: LazyLock<PhoneNumberUtil> = LazyLock::new(|| {
|
||||
// PhoneNumberUtil::new()
|
||||
// });
|
||||
/// Singleton instance of phone number util for general use
|
||||
pub static PHONE_NUMBER_UTIL: LazyLock<PhoneNumberUtil> = LazyLock::new(|| {
|
||||
PhoneNumberUtil::new()
|
||||
});
|
||||
@@ -1,3 +1,19 @@
|
||||
// Copyright (C) 2009 The Libphonenumber Authors
|
||||
// Copyright (C) 2025 The Kashin Vladislav (Rust adaptation author)
|
||||
//
|
||||
// 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.
|
||||
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use regex::Regex;
|
||||
@@ -8,6 +24,7 @@ use crate::{phonenumberutil::{helper_constants::{
|
||||
VALID_PUNCTUATION
|
||||
}, helper_functions::create_extn_pattern}, regexp_cache::RegexCache};
|
||||
|
||||
#[allow(unused)]
|
||||
pub(super) struct PhoneNumberRegExpsAndMappings {
|
||||
/// Regular expression of viable phone numbers. This is location independent.
|
||||
/// Checks we have at least three leading digits, and only valid punctuation,
|
||||
@@ -164,7 +181,13 @@ pub(super) struct PhoneNumberRegExpsAndMappings {
|
||||
/// followed by a single digit, separated by valid phone number punctuation.
|
||||
/// This prevents invalid punctuation (such as the star sign in Israeli star
|
||||
/// numbers) getting into the output of the AYTF.
|
||||
pub is_format_eligible_as_you_type_formatting_regex: Regex
|
||||
pub is_format_eligible_as_you_type_formatting_regex: Regex,
|
||||
|
||||
/// Added for function `formatting_rule_has_first_group_only`
|
||||
/// A pattern that is used to determine if the national prefix formatting rule
|
||||
/// has the first group only, i.e., does not start with the national prefix.
|
||||
/// Note that the pattern explicitly allows for unbalanced parentheses.
|
||||
pub formatting_rule_has_first_group_only_regex: Regex
|
||||
}
|
||||
|
||||
impl PhoneNumberRegExpsAndMappings {
|
||||
@@ -262,10 +285,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);
|
||||
@@ -299,13 +324,12 @@ impl PhoneNumberRegExpsAndMappings {
|
||||
separator_pattern: Regex::new(&format!("[{}]+", VALID_PUNCTUATION)).unwrap(),
|
||||
extn_patterns_for_matching: create_extn_pattern(false),
|
||||
extn_pattern: Regex::new(&format!("(?i)(?:{})$", &extn_patterns_for_parsing)).unwrap(),
|
||||
valid_phone_number_pattern: Regex::new(&format!("(?i){}(?:{})?",
|
||||
valid_phone_number_pattern: Regex::new(&format!("(?i)^(?:{})(?:{})?$",
|
||||
&valid_phone_number,
|
||||
extn_patterns_for_parsing
|
||||
)).unwrap(),
|
||||
valid_alpha_phone_pattern: Regex::new(&format!("(?i)(?:.*?[{}]){{3}}",
|
||||
VALID_ALPHA
|
||||
&extn_patterns_for_parsing
|
||||
)).unwrap(),
|
||||
// from java
|
||||
valid_alpha_phone_pattern: Regex::new("(?:.*?[A-Za-z]){3}.*").unwrap(),
|
||||
// The first_group_capturing_pattern was originally set to $1 but there
|
||||
// are some countries for which the first group is not used in the
|
||||
// national pattern (e.g. Argentina) so the $1 group does not match
|
||||
@@ -323,8 +347,17 @@ impl PhoneNumberRegExpsAndMappings {
|
||||
is_format_eligible_as_you_type_formatting_regex: Regex::new(
|
||||
&format!("[{}]*\\$1[{}]*(\\$\\d[{}]*)*",VALID_PUNCTUATION, VALID_PUNCTUATION, VALID_PUNCTUATION)
|
||||
).unwrap(),
|
||||
formatting_rule_has_first_group_only_regex: Regex::new("\\(?\\$1\\)?").unwrap()
|
||||
};
|
||||
instance.initialize_regexp_mappings();
|
||||
instance
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn check_regexps_are_compiling() {
|
||||
super::PhoneNumberRegExpsAndMappings::new();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,23 @@
|
||||
// Copyright (C) 2009 The Libphonenumber Authors
|
||||
// Copyright (C) 2025 The Kashin Vladislav (Rust adaptation author)
|
||||
//
|
||||
// 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.
|
||||
|
||||
|
||||
use log::{error};
|
||||
use super::regex_util::{RegexFullMatch, RegexConsume};
|
||||
|
||||
use crate::{interfaces, proto_gen::phonemetadata::PhoneNumberDesc, regexp_cache::{ErrorInvalidRegex, RegexCache}};
|
||||
use crate::{interfaces, generated::proto::phonemetadata::PhoneNumberDesc, regexp_cache::{InvalidRegexError, RegexCache}};
|
||||
|
||||
pub struct RegexBasedMatcher {
|
||||
cache: RegexCache,
|
||||
@@ -16,7 +32,7 @@ impl RegexBasedMatcher {
|
||||
&self, phone_number: &str,
|
||||
number_pattern: &str,
|
||||
allow_prefix_match: bool
|
||||
) -> Result<bool, ErrorInvalidRegex> {
|
||||
) -> Result<bool, InvalidRegexError> {
|
||||
let regexp = self.cache.get_regex(number_pattern)?;
|
||||
|
||||
// find first occurrence
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
// Copyright (C) 2009 The Libphonenumber Authors
|
||||
// Copyright (C) 2025 The Kashin Vladislav (Rust adaptation author)
|
||||
//
|
||||
// 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.
|
||||
|
||||
use regex::{Captures, Match, Regex};
|
||||
|
||||
pub trait RegexFullMatch {
|
||||
@@ -14,11 +29,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 +39,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)?;
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
// Copyright (C) 2009 The Libphonenumber Authors
|
||||
// Copyright (C) 2025 The Kashin Vladislav (Rust adaptation author)
|
||||
//
|
||||
// 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.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use dashmap::DashMap;
|
||||
@@ -5,26 +20,21 @@ use thiserror::Error;
|
||||
|
||||
#[derive(Debug, PartialEq, Error)]
|
||||
#[error("An error occurred while trying to create regex: {0}")]
|
||||
pub struct ErrorInvalidRegex(#[from] regex::Error);
|
||||
pub struct InvalidRegexError(#[from] regex::Error);
|
||||
|
||||
pub struct RegexCache {
|
||||
cache: DashMap<String, Arc<regex::Regex>>
|
||||
}
|
||||
|
||||
impl RegexCache {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
cache: DashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub fn with_capacity(capacity: usize) -> Self {
|
||||
Self {
|
||||
cache: DashMap::with_capacity(capacity),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_regex(&self, pattern: &str) -> Result<Arc<regex::Regex>, ErrorInvalidRegex> {
|
||||
pub fn get_regex(&self, pattern: &str) -> Result<Arc<regex::Regex>, InvalidRegexError> {
|
||||
if let Some(regex) = self.cache.get(pattern) {
|
||||
Ok(regex.value().clone())
|
||||
} else {
|
||||
|
||||
28
src/region_code.rs
Normal file
28
src/region_code.rs
Normal file
@@ -0,0 +1,28 @@
|
||||
// Copyright (C) 2009 The Libphonenumber Authors
|
||||
// Copyright (C) 2025 The Kashin Vladislav (Rust adaptation author)
|
||||
//
|
||||
// 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 struct RegionCode {
|
||||
}
|
||||
|
||||
impl RegionCode {
|
||||
/// Returns a region code string representing the "unknown" region.
|
||||
pub fn get_unknown() -> &'static str {
|
||||
return Self::zz();
|
||||
}
|
||||
|
||||
pub fn zz() -> &'static str {
|
||||
return "ZZ";
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,17 @@
|
||||
// Copyright (C) 2025 The Kashin Vladislav (Rust adaptation author)
|
||||
//
|
||||
// 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.
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
/// Strips prefix of given string Cow. Returns option with `Some` if
|
||||
|
||||
3
src/tests/mod.rs
Normal file
3
src/tests/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
#[cfg(test)]
|
||||
mod phonenumberutil_tests;
|
||||
pub(self) mod region_code;
|
||||
3235
src/tests/phonenumberutil_tests.rs
Normal file
3235
src/tests/phonenumberutil_tests.rs
Normal file
File diff suppressed because it is too large
Load Diff
161
src/tests/region_code.rs
Normal file
161
src/tests/region_code.rs
Normal file
@@ -0,0 +1,161 @@
|
||||
pub struct RegionCode {}
|
||||
|
||||
#[allow(unused)]
|
||||
impl RegionCode {
|
||||
pub fn ad() -> &'static str {
|
||||
"AD"
|
||||
}
|
||||
|
||||
pub fn ae() -> &'static str {
|
||||
"AE"
|
||||
}
|
||||
|
||||
pub fn am() -> &'static str {
|
||||
"AM"
|
||||
}
|
||||
|
||||
pub fn ao() -> &'static str {
|
||||
"AO"
|
||||
}
|
||||
|
||||
pub fn aq() -> &'static str {
|
||||
"AQ"
|
||||
}
|
||||
|
||||
pub fn ar() -> &'static str {
|
||||
"AR"
|
||||
}
|
||||
|
||||
pub fn au() -> &'static str {
|
||||
"AU"
|
||||
}
|
||||
|
||||
pub fn bb() -> &'static str {
|
||||
"BB"
|
||||
}
|
||||
|
||||
pub fn br() -> &'static str {
|
||||
"BR"
|
||||
}
|
||||
|
||||
pub fn bs() -> &'static str {
|
||||
"BS"
|
||||
}
|
||||
|
||||
pub fn by() -> &'static str {
|
||||
"BY"
|
||||
}
|
||||
|
||||
pub fn ca() -> &'static str {
|
||||
"CA"
|
||||
}
|
||||
|
||||
pub fn ch() -> &'static str {
|
||||
"CH"
|
||||
}
|
||||
|
||||
pub fn cl() -> &'static str {
|
||||
"CL"
|
||||
}
|
||||
|
||||
pub fn cn() -> &'static str {
|
||||
"CN"
|
||||
}
|
||||
|
||||
pub fn co() -> &'static str {
|
||||
"CO"
|
||||
}
|
||||
|
||||
pub fn cs() -> &'static str {
|
||||
"CS"
|
||||
}
|
||||
|
||||
pub fn cx() -> &'static str {
|
||||
"CX"
|
||||
}
|
||||
|
||||
pub fn de() -> &'static str {
|
||||
"DE"
|
||||
}
|
||||
|
||||
pub fn fr() -> &'static str {
|
||||
"FR"
|
||||
}
|
||||
|
||||
pub fn gb() -> &'static str {
|
||||
"GB"
|
||||
}
|
||||
|
||||
pub fn hu() -> &'static str {
|
||||
"HU"
|
||||
}
|
||||
|
||||
pub fn it() -> &'static str {
|
||||
"IT"
|
||||
}
|
||||
|
||||
pub fn jp() -> &'static str {
|
||||
"JP"
|
||||
}
|
||||
|
||||
pub fn kr() -> &'static str {
|
||||
"KR"
|
||||
}
|
||||
|
||||
pub fn mx() -> &'static str {
|
||||
"MX"
|
||||
}
|
||||
|
||||
pub fn nz() -> &'static str {
|
||||
"NZ"
|
||||
}
|
||||
|
||||
pub fn pl() -> &'static str {
|
||||
"PL"
|
||||
}
|
||||
|
||||
pub fn re() -> &'static str {
|
||||
"RE"
|
||||
}
|
||||
|
||||
pub fn ru() -> &'static str {
|
||||
"RU"
|
||||
}
|
||||
|
||||
pub fn se() -> &'static str {
|
||||
"SE"
|
||||
}
|
||||
|
||||
pub fn sg() -> &'static str {
|
||||
"SG"
|
||||
}
|
||||
|
||||
pub fn un001() -> &'static str {
|
||||
"001"
|
||||
}
|
||||
|
||||
pub fn us() -> &'static str {
|
||||
"US"
|
||||
}
|
||||
|
||||
pub fn uz() -> &'static str {
|
||||
"UZ"
|
||||
}
|
||||
|
||||
pub fn yt() -> &'static str {
|
||||
"YT"
|
||||
}
|
||||
|
||||
pub fn zw() -> &'static str {
|
||||
"ZW"
|
||||
}
|
||||
|
||||
/// s a region code string representing the "unknown" region.
|
||||
pub fn get_unknown() -> &'static str {
|
||||
Self::zz()
|
||||
}
|
||||
|
||||
pub fn zz() -> &'static str {
|
||||
"ZZ"
|
||||
}
|
||||
}
|
||||
3
tools/java/.gitignore
vendored
Normal file
3
tools/java/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
target
|
||||
generated
|
||||
bin
|
||||
11
tools/java/Readme.md
Normal file
11
tools/java/Readme.md
Normal 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
78
tools/java/common/pom.xml
Normal 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>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
45
tools/java/pom.xml
Normal 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>
|
||||
160
tools/java/rust-build/pom.xml
Normal file
160
tools/java/rust-build/pom.xml
Normal 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>
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
75
tools/scripts/generate_metadata.sh
Normal file
75
tools/scripts/generate_metadata.sh
Normal file
@@ -0,0 +1,75 @@
|
||||
#!/bin/bash
|
||||
|
||||
filedir="./$(dirname "$0")"
|
||||
javadir="$filedir/../java"
|
||||
project_home="$filedir/../.."
|
||||
generated_dir="$project_home/src/generated/metadata"
|
||||
echo $generated_dir
|
||||
|
||||
resources_dir="$project_home/resources"
|
||||
rust_build_jar="$javadir/rust-build/target/rust-build-1.0-SNAPSHOT-jar-with-dependencies.jar"
|
||||
|
||||
copyright_header="\
|
||||
// Copyright (C) 2009 The Libphonenumber Authors
|
||||
// Copyright (C) 2025 The Kashin Vladislav (Rust adaptation author)
|
||||
//
|
||||
// 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.
|
||||
"
|
||||
|
||||
skip_install=false
|
||||
|
||||
# Loop through all the command-line arguments
|
||||
for arg in "$@"
|
||||
do
|
||||
if [ "$arg" == "--skip-install" ]
|
||||
then
|
||||
skip_install=true
|
||||
# You can break the loop once the flag is found if you don't need to process further arguments
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ $skip_install == false ]]; then
|
||||
mvn -f "$javadir/pom.xml" install
|
||||
fi
|
||||
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 test metadata
|
||||
generate "PhoneNumberMetadataForTesting.xml" "test_metadata" "metadata" "TEST_METADATA"
|
||||
|
||||
# remove unnecessary nesting with pub use
|
||||
echo "\
|
||||
$copyright_header
|
||||
|
||||
mod metadata;
|
||||
|
||||
// use only in test case
|
||||
#[cfg(test)]
|
||||
mod test_metadata;
|
||||
|
||||
pub use metadata::METADATA;
|
||||
#[cfg(test)]
|
||||
pub use test_metadata::TEST_METADATA;
|
||||
" > "$generated_dir/mod.rs"
|
||||
Reference in New Issue
Block a user