Skip to main content

bt_hfp/call/
number.rs

1// Copyright 2021 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use thiserror::Error;
6
7#[derive(Debug, Clone, PartialEq, Hash, Eq, Error)]
8pub enum NumberError {
9    #[error("Number contains control characters")]
10    ControlCharacters,
11    #[error("Number contains internal quotes")]
12    InternalQuotes,
13    #[error("Number is not enclosed in delimiting quotes")]
14    NotQuoted,
15}
16
17/// The fuchsia.bluetooth.hfp library representation of a Number.
18pub type FidlNumber = String;
19/// A phone number.  Clients should generally use `as_non_at_string` and
20/// `from_non_at_string` to work with these, which add and remove delimiting
21/// quotes.  As AT commands require these quotes to be in place around numbers,
22/// when generating and parsing AT commands, clients should use `from_at_string`
23/// and `as_at_string`, which maintain the quotes.
24#[derive(Debug, Clone, PartialEq, Hash, Default, Eq)]
25pub struct Number(String);
26
27fn is_quoted(s: &str) -> bool {
28    s.starts_with('"') && s.ends_with('"') && s.len() >= 2
29}
30
31impl Number {
32    /// Format value indicating no changes on the number presentation are required.
33    /// See HFP v1.8, Section 4.34.2.
34    const NUMBER_FORMAT: i64 = 129;
35
36    /// Returns the numeric representation of the Number's format as specified in HFP v1.8,
37    /// Section 4.34.2.
38    pub fn type_(&self) -> i64 {
39        Number::NUMBER_FORMAT
40    }
41
42    /// Converts the Number to a String, stripping quotes from the beginning and end.
43    pub fn to_non_at_string(&self) -> String {
44        if is_quoted(&self.0) {
45            let string = self.0.clone();
46            let mut chars = string.chars();
47            let _front_must_exist = chars.next();
48            let _back_must_exist = chars.next_back();
49            String::from(chars.as_str())
50        } else {
51            self.0.clone()
52        }
53    }
54
55    /// Converts the Number to a String to be used in AT commands, leaving the delimiting quotes in
56    /// place.
57    pub fn to_at_string(&self) -> String {
58        self.0.clone()
59    }
60
61    pub fn from_non_at_string(s: &str) -> Result<Self, NumberError> {
62        if is_quoted(s) {
63            Self::from_at_string(s)
64        } else {
65            let quoted = format!("\"{}\"", s);
66            Self::from_at_string(&quoted)
67        }
68    }
69
70    /// Converts a String to a Number, from an AT command, leaving the delimiting quotes in place.
71    /// Returns an error if the string contains ASCII control characters or internal quotes.
72    pub fn from_at_string(s: &str) -> Result<Self, NumberError> {
73        if s.chars().any(|c| c.is_ascii_control()) {
74            return Err(NumberError::ControlCharacters);
75        }
76
77        // It is valid for the phone number to be completely empty/omitted in HFP/AT.
78        if s.is_empty() {
79            return Ok(Self(String::from(s)));
80        }
81        if !is_quoted(s) {
82            return Err(NumberError::NotQuoted);
83        }
84
85        let inner_s = &s[1..s.len() - 1];
86
87        if inner_s.contains('"') {
88            return Err(NumberError::InternalQuotes);
89        }
90
91        Ok(Self(String::from(s)))
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98
99    #[fuchsia::test]
100    fn number_type_in_valid_range() {
101        let number = Number(String::from("\"1234567\""));
102        // type values must be in range 128-175.
103        assert!(number.type_() >= 128);
104        assert!(number.type_() <= 175);
105    }
106
107    #[fuchsia::test]
108    fn number_str_delimiters() {
109        // Convert str to Number
110        {
111            let actual_number = Number::from_non_at_string("1234567").unwrap();
112            let expected_number = Number(String::from("\"1234567\""));
113            assert_eq!(actual_number, expected_number);
114        }
115
116        // Convert Number to str
117        {
118            let actual_string = Number(String::from("\"1234567\"")).to_non_at_string();
119            let expected_string = String::from("1234567");
120            assert_eq!(actual_string, expected_string);
121        }
122
123        // Convert str to Number with redundant quotes
124        {
125            let actual_number = Number::from_non_at_string("\"1234567\"").unwrap();
126            let expected_number = Number(String::from("\"1234567\""));
127            assert_eq!(actual_number, expected_number);
128        }
129
130        // Convert AT command str to Number
131        {
132            let actual_number = Number::from_at_string("\"1234567\"").unwrap();
133            let expected_number = Number(String::from("\"1234567\""));
134            assert_eq!(actual_number, expected_number);
135        }
136
137        // Convert Number to AT command str
138        {
139            let actual_string = Number(String::from("\"1234567\"")).to_at_string();
140            let expected_string = String::from("\"1234567\"");
141            assert_eq!(actual_string, expected_string);
142        }
143    }
144
145    #[fuchsia::test]
146    fn number_validation_success() {
147        assert!(Number::from_non_at_string("").is_ok());
148        assert!(Number::from_non_at_string("123456").is_ok());
149        assert!(Number::from_non_at_string("\"123456\"").is_ok());
150        assert!(Number::from_at_string("").is_ok());
151        assert!(Number::from_at_string("\"123456\"").is_ok());
152    }
153
154    #[fuchsia::test]
155    fn number_validation_error() {
156        // Control characters are rejected
157        assert!(matches!(
158            Number::from_non_at_string("123\r\n456"),
159            Err(NumberError::ControlCharacters)
160        ));
161        assert!(matches!(
162            Number::from_at_string("\"123\0456\""),
163            Err(NumberError::ControlCharacters)
164        ));
165
166        // Not quoted is rejected for from_at_string
167        assert!(matches!(Number::from_at_string("123456"), Err(NumberError::NotQuoted)));
168
169        // Internal quotes are rejected
170        assert!(matches!(Number::from_non_at_string("123\"456"), Err(NumberError::InternalQuotes)));
171        assert!(matches!(
172            Number::from_non_at_string("\"123\"456\""),
173            Err(NumberError::InternalQuotes)
174        ));
175        assert!(matches!(Number::from_at_string("\"123\"456\""), Err(NumberError::InternalQuotes)));
176    }
177}