Compare commits

...

35 Commits

Author SHA1 Message Date
Vlasislav Kashin
519148ffd9 Update readme 2025-07-13 21:32:08 +03:00
Vlasislav Kashin
6be301ebd8 Refactor names and imports 2025-07-13 21:29:41 +03:00
Vlasislav Kashin
76a8d4857f Refactor exports and imports 2025-07-13 21:24:52 +03:00
Vlasislav Kashin
7f7bab7f16 Reorganize imports and exports 2025-07-13 21:16:14 +03:00
Vlasislav Kashin
71d2562d83 Add readme, license 2025-07-13 19:25:17 +03:00
Vlasislav Kashin
9c67b42e9c Revert fixes 2025-07-13 18:57:38 +03:00
Vlasislav Kashin
56734bcb1c Add documentation, minor fixes 2025-07-13 18:55:50 +03:00
Vlasislav Kashin
b64c063563 * 2025-07-13 17:51:02 +03:00
Vlasislav Kashin
da9f5e9198 Add copyright header 2025-07-13 17:50:43 +03:00
Vlasislav Kashin
0ceb7c6c8c Add skip install flag to generate_metadata script 2025-07-13 17:47:05 +03:00
Vlasislav Kashin
b979b290b8 Sanitize project 2025-07-13 17:37:30 +03:00
Vlasislav Kashin
923a941473 Update tests 2025-07-13 17:30:47 +03:00
Vlasislav Kashin
03911c0572 Add more tests, better naming and bug fixes 2025-07-13 17:07:40 +03:00
Vlasislav Kashin
77fa0e2b09 Sanitaze code 2025-07-13 16:24:00 +03:00
Vlasislav Kashin
f646fe4605 Update generate metadata script 2025-07-13 15:03:02 +03:00
Vlasislav Kashin
cb5f0d8fcc Refactor build script 2025-07-13 15:01:40 +03:00
Vlasislav Kashin
3a2e8e6c0f Move helper constants out of folder 2025-07-13 14:59:39 +03:00
Vlasislav Kashin
467416e3ef Update generated location 2025-07-13 14:58:49 +03:00
Vlasislav Kashin
1464119ff8 Better error naming 2025-07-13 14:52:06 +03:00
Vlasislav Kashin
beae04dee8 Add more tests, better error handling 2025-07-13 14:49:56 +03:00
Vlasislav Kashin
ebe7d236e9 feat: update regex, bug fixes, add tests 2025-07-12 23:30:44 +03:00
Vlasislav Kashin
2fea8f1e20 Phonenumberutil: add is_alpha_number 2025-07-12 21:21:32 +03:00
Vlasislav Kashin
392c793d5c Update phonenumberutil get_national_significant_number - &self reciever 2025-07-12 20:59:17 +03:00
Vlasislav Kashin
e7daffa6f7 helper_constants: fix const REGION_CODE_FOR_NON_GEO_ENTITY 2025-07-12 20:35:33 +03:00
Vlasislav Kashin
10c5ee1159 Java: added compiled metadata generation 2025-07-11 03:46:50 +03:00
Vlasislav Kashin
8a42c0ecb5 Initial copy of buildMetadata 2025-07-11 00:49:56 +03:00
Vlasislav Kashin
e75eda86e6 Add tests 2025-07-10 18:37:54 +03:00
Vlasislav Kashin
1bb46ac1b7 Fix valid phonenumber regex 2025-07-10 18:37:47 +03:00
Vlasislav Kashin
0e683eac90 Add metadata constructor for PhoneNumberUtil 2025-07-10 18:37:23 +03:00
Vlasislav Kashin
464711df8c update up to dec_from_char v0.2.0 2025-07-10 14:40:49 +03:00
Vlasislav Kashin
e52a19e6c1 Remove unused, refactor exports 2025-07-10 13:00:41 +03:00
Vlasislav Kashin
e72187d2d7 Better None handling: Remove unwrap 2025-07-10 12:50:12 +03:00
Vlasislav Kashin
457bb65b9a Better error handling 2025-07-10 12:41:21 +03:00
Vlasislav Kashin
3f07806990 Ugrade methods to return iterators 2025-07-10 11:29:18 +03:00
Vlasislav Kashin
4f5571f7a7 Bug fixes and upgrades 2025-07-10 10:44:11 +03:00
57 changed files with 11935 additions and 1324 deletions

1
.gitignore vendored
View File

@@ -1 +1,2 @@
/target
.vscode

872
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -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
View 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
View File

@@ -0,0 +1,121 @@
# libphonenumber-rust
[![Crates.io](https://img.shields.io/crates/v/rlibphonenumber.svg)](https://crates.io/crates/rlibphonenumber)
[![Docs.rs](https://docs.rs/phonenumber/badge.svg)](https://docs.rs/rlibphonenumber)
[![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](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
View 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
View 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);

View File

@@ -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(())
}

View File

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

View File

@@ -0,0 +1,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;

File diff suppressed because it is too large Load Diff

View File

@@ -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;

View File

@@ -1,3 +0,0 @@
mod region_code;
pub use region_code::RegionCode;

View File

@@ -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";
}
}

View File

@@ -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

View File

@@ -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;

View File

@@ -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

View File

@@ -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

View File

@@ -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,
}

View File

@@ -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;

View File

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

View File

@@ -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,

View File

@@ -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() }
}
}

View File

@@ -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()
});

View File

@@ -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

View File

@@ -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

View File

@@ -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)?;

View File

@@ -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
View 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";
}
}

View File

View File

@@ -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
View File

@@ -0,0 +1,3 @@
#[cfg(test)]
mod phonenumberutil_tests;
pub(self) mod region_code;

File diff suppressed because it is too large Load Diff

161
src/tests/region_code.rs Normal file
View 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
View File

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,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"