1
//! A crate for performing GeoIP lookups using the Tor GeoIP database.
2

            
3
// @@ begin lint list maintained by maint/add_warning @@
4
#![allow(renamed_and_removed_lints)] // @@REMOVE_WHEN(ci_arti_stable)
5
#![allow(unknown_lints)] // @@REMOVE_WHEN(ci_arti_nightly)
6
#![warn(missing_docs)]
7
#![warn(noop_method_call)]
8
#![warn(unreachable_pub)]
9
#![warn(clippy::all)]
10
#![deny(clippy::await_holding_lock)]
11
#![deny(clippy::cargo_common_metadata)]
12
#![deny(clippy::cast_lossless)]
13
#![deny(clippy::checked_conversions)]
14
#![allow(clippy::cognitive_complexity)] // See arti#2556
15
#![deny(clippy::debug_assert_with_mut_call)]
16
#![deny(clippy::exhaustive_enums)]
17
#![deny(clippy::exhaustive_structs)]
18
#![deny(clippy::expl_impl_clone_on_copy)]
19
#![deny(clippy::fallible_impl_from)]
20
#![deny(clippy::implicit_clone)]
21
#![deny(clippy::large_stack_arrays)]
22
#![warn(clippy::manual_ok_or)]
23
#![deny(clippy::missing_docs_in_private_items)]
24
#![warn(clippy::needless_borrow)]
25
#![warn(clippy::needless_pass_by_value)]
26
#![warn(clippy::option_option)]
27
#![deny(clippy::print_stderr)]
28
#![deny(clippy::print_stdout)]
29
#![warn(clippy::rc_buffer)]
30
#![deny(clippy::ref_option_ref)]
31
#![warn(clippy::semicolon_if_nothing_returned)]
32
#![warn(clippy::trait_duplication_in_bounds)]
33
#![deny(clippy::unchecked_time_subtraction)]
34
#![deny(clippy::unnecessary_wraps)]
35
#![warn(clippy::unseparated_literal_suffix)]
36
#![deny(clippy::unwrap_used)]
37
#![deny(clippy::mod_module_files)]
38
#![allow(clippy::let_unit_value)] // This can reasonably be done for explicitness
39
#![allow(clippy::uninlined_format_args)]
40
#![allow(clippy::significant_drop_in_scrutinee)] // arti/-/merge_requests/588/#note_2812945
41
#![allow(clippy::result_large_err)] // temporary workaround for arti#587
42
#![allow(clippy::needless_raw_string_hashes)] // complained-about code is fine, often best
43
#![allow(clippy::needless_lifetimes)] // See arti#1765
44
#![allow(mismatched_lifetime_syntaxes)] // temporary workaround for arti#2060
45
#![allow(clippy::collapsible_if)] // See arti#2342
46
#![deny(clippy::unused_async)]
47
#![deny(clippy::string_slice)] // See arti#2571
48
#![allow(recursion_depth_exceeding_limit)] // arti#2715, rust/issues/159228
49
//! <!-- @@ end lint list maintained by maint/add_warning @@ -->
50

            
51
// TODO #1645 (either remove this, or decide to have it everywhere)
52
#![cfg_attr(not(all(feature = "full")), allow(unused))]
53

            
54
use crate::dense_range_map::DenseRangeMap;
55
pub use crate::err::Error;
56
use std::fmt::{Debug, Display, Formatter};
57
use std::net::{IpAddr, Ipv6Addr};
58
use std::num::{NonZeroU16, NonZeroU32};
59
use std::ops::RangeInclusive;
60
use std::str::FromStr;
61
use std::sync::{Arc, OnceLock};
62

            
63
mod dense_range_map;
64
mod err;
65

            
66
/// A parsed copy of the embedded database.
67
#[cfg(feature = "embedded-db")]
68
static EMBEDDED_DB_PARSED: OnceLock<Arc<GeoipDb>> = OnceLock::new();
69

            
70
/// A two-letter country code.
71
///
72
/// Specifically, this type represents a purported "ISO 3166-1 alpha-2" country
73
/// code, such as "IT" for Italy or "UY" for Uruguay.
74
///
75
/// It does not include the sentinel value `??` that we use to represent
76
/// "country unknown"; if you need that, use [`OptionCc`]. Other than that, we
77
/// do not check whether the country code represents a real country: we only
78
/// ensure that it is a pair of printing ASCII characters.
79
///
80
/// Note that the geoip databases included with Arti will only include real
81
/// countries; we do not include the pseudo-countries `A1` through `An` for
82
/// "anonymous proxies", since doing so would mean putting nearly all Tor relays
83
/// into one of those countries.
84
#[derive(Copy, Clone, Eq, PartialEq)]
85
#[repr(transparent)]
86
pub struct CountryCode {
87
    /// The underlying value (two printable ASCII characters, stored uppercase).
88
    ///
89
    /// The special value `??` is excluded, since it is not a country; use
90
    /// `OptionCc` instead if you need to represent that.
91
    ///
92
    /// We store these as `NonZeroU16` so that an `Option<CountryCode>` only has to
93
    /// take 2 bytes. This helps with alignment and storage.
94
    ///
95
    /// (We use a `NonZeroU16` rather than `[NonZeroU8; 2]` to ensure that every
96
    /// bit representation is a valid `Option<CountryCode>`.)
97
    inner: NonZeroU16,
98
}
99

            
100
impl CountryCode {
101
    /// Make a new `CountryCode`.
102
197
    fn new(cc_orig: &str) -> Result<Self, Error> {
103
        /// Try to convert an array of 2 bytes into a NonZeroU16.
104
        #[inline]
105
179
        fn try_cvt_to_nz(inp: [u8; 2]) -> Result<NonZeroU16, Error> {
106
179
            if inp[0] == 0 || inp[1] == 0 {
107
                return Err(Error::BadCountryCode("Country code contained NULs".into()));
108
179
            }
109
179
            Ok(u16::from_ne_bytes(inp)
110
179
                .try_into()
111
179
                .expect("zero arrived surprisingly"))
112
179
        }
113

            
114
197
        let cc = cc_orig.to_ascii_uppercase();
115

            
116
197
        let cc: [u8; 2] = cc
117
197
            .as_bytes()
118
197
            .try_into()
119
200
            .map_err(|_| Error::BadCountryCode(cc))?;
120

            
121
398
        if !cc.iter().all(|b| b.is_ascii() && !b.is_ascii_control()) {
122
6
            return Err(Error::BadCountryCode(cc_orig.to_owned()));
123
185
        }
124

            
125
185
        if &cc == b"??" {
126
6
            return Err(Error::NowhereNotSupported);
127
179
        }
128

            
129
        Ok(Self {
130
179
            inner: try_cvt_to_nz(cc).map_err(|_| Error::BadCountryCode(cc_orig.to_owned()))?,
131
        })
132
197
    }
133

            
134
    /// Get the actual country code.
135
    ///
136
    /// This just calls `.as_ref()`.
137
    pub fn get(&self) -> &str {
138
        self.as_ref()
139
    }
140
}
141

            
142
impl Display for CountryCode {
143
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
144
        write!(f, "{}", self.as_ref())
145
    }
146
}
147

            
148
impl Debug for CountryCode {
149
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
150
        write!(f, "CountryCode(\"{}\")", self.as_ref())
151
    }
152
}
153

            
154
impl AsRef<str> for CountryCode {
155
112
    fn as_ref(&self) -> &str {
156
        /// Convert a reference to a NonZeroU16 to a reference to
157
        /// an array of 2 bytes.
158
        #[inline]
159
112
        fn cvt_ref(inp: &NonZeroU16) -> &[u8; 2] {
160
            // SAFETY: Every NonZeroU16 has a layout, alignment, and bit validity that is
161
            // also a valid [u8; 2].  The layout of arrays is also guaranteed.
162
            //
163
            // (We don't use try_into here because we need to return a str that
164
            // points to a reference to self.)
165
112
            let slice: &[NonZeroU16] = std::slice::from_ref(inp);
166
112
            let (_, slice, _) = unsafe { slice.align_to::<u8>() };
167
112
            slice
168
112
                .try_into()
169
112
                .expect("the resulting slice should have the correct length!")
170
112
        }
171

            
172
        // This shouldn't ever panic, since we shouldn't feed non-utf8 country
173
        // codes in.
174
        //
175
        // In theory we could use from_utf8_unchecked, but that's probably not
176
        // needed.
177
112
        std::str::from_utf8(cvt_ref(&self.inner)).expect("invalid country code in CountryCode")
178
112
    }
179
}
180

            
181
impl FromStr for CountryCode {
182
    type Err = Error;
183

            
184
32
    fn from_str(s: &str) -> Result<Self, Self::Err> {
185
32
        CountryCode::new(s)
186
32
    }
187
}
188

            
189
/// Wrapper for an `Option<`[`CountryCode`]`>` that encodes `None` as `??`.
190
///
191
/// Used so that we can implement foreign traits.
192
#[derive(
193
    Copy, Clone, Debug, Eq, PartialEq, derive_more::Into, derive_more::From, derive_more::AsRef,
194
)]
195
#[allow(clippy::exhaustive_structs)]
196
pub struct OptionCc(pub Option<CountryCode>);
197

            
198
impl FromStr for OptionCc {
199
    type Err = Error;
200

            
201
163
    fn from_str(s: &str) -> Result<Self, Self::Err> {
202
163
        match CountryCode::new(s) {
203
4
            Err(Error::NowhereNotSupported) => Ok(None.into()),
204
            Err(e) => Err(e),
205
159
            Ok(cc) => Ok(Some(cc).into()),
206
        }
207
163
    }
208
}
209

            
210
impl Display for OptionCc {
211
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
212
        match self.0 {
213
            Some(cc) => write!(f, "{}", cc),
214
            None => write!(f, "??"),
215
        }
216
    }
217
}
218

            
219
/// The type of an ASN.
220
type Asn = NonZeroU32;
221

            
222
/// A database of IP addresses to country codes.
223
#[derive(Clone, Eq, PartialEq, Debug)]
224
pub struct GeoipDb {
225
    /// The IPv4 subset of the database, with v4 addresses stored as 32-bit integers.
226
    map_v4: DenseRangeMap<u32, CountryCode, Asn>,
227
    /// The IPv6 subset of the database, with v6 addresses stored as 128-bit integers.
228
    map_v6: DenseRangeMap<u128, CountryCode, Asn>,
229
}
230

            
231
impl GeoipDb {
232
    /// Make a new `GeoipDb` using a compiled-in copy of the GeoIP database.
233
    ///
234
    /// The returned instance of the database is shared with `Arc` across all invocations of this
235
    /// function in the same program.
236
    #[cfg(feature = "embedded-db")]
237
155
    pub fn new_embedded() -> Arc<Self> {
238
157
        Arc::clone(EMBEDDED_DB_PARSED.get_or_init(|| {
239
            use tor_geoip_db as db;
240
106
            fn cvt_ccs(ccs: &'static [Option<NonZeroU16>]) -> &'static [Option<CountryCode>] {
241
                // SAFETY: CountryCode is a repr(transparent) for NonZeroU16.
242
106
                let (pre, data, post) = unsafe { ccs.align_to::<Option<CountryCode>>() };
243
106
                assert!(pre.is_empty());
244
106
                assert!(post.is_empty());
245
106
                data
246
106
            }
247

            
248
53
            let map_v4 = DenseRangeMap::from_static_parts(db::ipv4s(), cvt_ccs(db::ipv4c()), None);
249
53
            let map_v6 = DenseRangeMap::from_static_parts(db::ipv6s(), cvt_ccs(db::ipv6c()), None);
250

            
251
53
            Arc::new(
252
                // It's reasonable to assume the one we embedded is fine --
253
                // we'll test it in CI, etc.
254
53
                GeoipDb { map_v4, map_v6 },
255
            )
256
53
        }))
257
155
    }
258

            
259
    /// Make a new `GeoipDb` using provided copies of the v4 and v6 database, in Tor legacy format.
260
53
    pub fn new_from_legacy_format(
261
53
        db_v4: &str,
262
53
        db_v6: &str,
263
53
        include_asn: bool,
264
53
    ) -> Result<Self, Error> {
265
53
        let discard_asn = !include_asn;
266
53
        let map_v4 = DenseRangeMap::try_from_sorted_inclusive_ranges(
267
53
            db_v4
268
53
                .lines()
269
58
                .filter_map(|line| parse_line::<u32>(line).transpose()),
270
53
            discard_asn,
271
        )?;
272

            
273
53
        let map_v6 = DenseRangeMap::try_from_sorted_inclusive_ranges(
274
53
            db_v6
275
53
                .lines()
276
265
                .filter_map(|line| parse_line::<Ipv6Addr>(line).transpose()),
277
53
            discard_asn,
278
        )?;
279

            
280
53
        Ok(Self { map_v4, map_v6 })
281
53
    }
282

            
283
    /// Return the database in a raw format suitable for embedding.
284
    ///
285
    /// This method and the format it returns are unstable.
286
    /// This method should only be used for maintaining the database.
287
    #[cfg(feature = "export")]
288
    #[allow(clippy::type_complexity)]
289
    pub fn export_raw(&self) -> RawGeoipDbExport {
290
        let (ipv4_starts, ipv4_ccs, ipv4_asns) = self.map_v4.export();
291
        let (ipv6_starts, ipv6_ccs, ipv6_asns) = self.map_v6.export();
292

            
293
        RawGeoipDbExport {
294
            ipv4_starts,
295
            ipv4_ccs,
296
            ipv4_asns,
297
            ipv6_starts,
298
            ipv6_ccs,
299
            ipv6_asns,
300
        }
301
    }
302

            
303
    /// Get a 2-letter country code for the given IP address, if this data is available.
304
3329
    pub fn lookup_country_code(&self, ip: IpAddr) -> Option<&CountryCode> {
305
3329
        match ip {
306
2760
            IpAddr::V4(v4) => self.map_v4.get1(&v4.into()),
307
569
            IpAddr::V6(v6) => self.map_v6.get1(&v6.into()),
308
        }
309
3329
    }
310

            
311
    /// Determine a 2-letter country code for a host with multiple IP addresses.
312
    ///
313
    /// This looks up all of the IP addresses with `lookup_country_code`. If the lookups
314
    /// return different countries, `None` is returned. IP addresses that fail to resolve
315
    /// into a country are ignored if some of the other addresses do resolve successfully.
316
752
    pub fn lookup_country_code_multi<I>(&self, ips: I) -> Option<&CountryCode>
317
752
    where
318
752
        I: IntoIterator<Item = IpAddr>,
319
    {
320
752
        let mut ret = None;
321

            
322
1050
        for ip in ips {
323
1050
            if let Some(cc) = self.lookup_country_code(ip) {
324
                // If we already have a return value and it's different, then return None;
325
                // a server can't be in two different countries.
326
10
                if ret.is_some() && ret != Some(cc) {
327
2
                    return None;
328
8
                }
329

            
330
8
                ret = Some(cc);
331
1040
            }
332
        }
333

            
334
750
        ret
335
752
    }
336

            
337
    /// Return the ASN the IP address is in, if this data is available.
338
    pub fn lookup_asn(&self, ip: IpAddr) -> Option<u32> {
339
        let cc = match ip {
340
            IpAddr::V4(v4) => self.map_v4.get2(&v4.into()),
341
            IpAddr::V6(v6) => self.map_v6.get2(&v6.into()),
342
        };
343
        cc.map(|nz| nz.get())
344
    }
345
}
346

            
347
/// A type that can be an address entry in one of our databases.
348
trait DbAddress: FromStr {
349
    /// The integer that we use to represent this kind of address.
350
    type Int;
351

            
352
    /// Convert this address to an integer.
353
    fn to_int(&self) -> Self::Int;
354
}
355

            
356
impl DbAddress for u32 {
357
    type Int = u32;
358

            
359
4
    fn to_int(&self) -> Self::Int {
360
4
        *self
361
4
    }
362
}
363

            
364
impl DbAddress for Ipv6Addr {
365
    type Int = u128;
366

            
367
314
    fn to_int(&self) -> Self::Int {
368
314
        (*self).into()
369
314
    }
370
}
371

            
372
/// A line as returned by [`parse_line`].
373
type ParsedLine<T> = (RangeInclusive<T>, Option<CountryCode>, Option<Asn>);
374

            
375
/// Parse a single line from a database, expecting addresses of type T.
376
///
377
/// Return Ok(None) if the line is empty.
378
269
fn parse_line<T: DbAddress>(line: &str) -> Result<Option<ParsedLine<T::Int>>, Error>
379
269
where
380
269
    Error: From<<T as FromStr>::Err>,
381
{
382
269
    if line.starts_with('#') {
383
        return Ok(None);
384
269
    }
385
269
    let line = line.trim();
386
269
    if line.is_empty() {
387
110
        return Ok(None);
388
159
    }
389

            
390
159
    let mut split = line.split(',');
391
159
    let from = split
392
159
        .next()
393
159
        .ok_or(Error::BadFormat("empty line somehow?".into()))?
394
159
        .parse::<T>()?
395
159
        .to_int();
396
159
    let to = split
397
159
        .next()
398
159
        .ok_or(Error::BadFormat("line with insufficient commas".into()))?
399
159
        .parse::<T>()?
400
159
        .to_int();
401
159
    let cc = split
402
159
        .next()
403
159
        .ok_or(Error::BadFormat("line with insufficient commas".into()))?;
404
159
    let cc = match cc {
405
159
        "" => None,
406
159
        cc => OptionCc::from_str(cc)?.0,
407
    };
408
159
    let asn = split.next().map(|x| x.parse::<u32>()).transpose()?;
409
    // Treat "0" as "no asn".
410
159
    let asn = asn.map(NonZeroU32::try_from).transpose().ok().flatten();
411

            
412
159
    Ok(Some((from..=to, cc, asn)))
413
269
}
414

            
415
/// A (representation of a) host on the network which may have a known country code.
416
pub trait HasCountryCode {
417
    /// Return the country code in which this server is most likely located.
418
    ///
419
    /// This is usually implemented by simple GeoIP lookup on the addresses provided by `HasAddrs`.
420
    /// It follows that the server might not actually be in the returned country, but this is a
421
    /// halfway decent estimate for what other servers might guess the server's location to be
422
    /// (and thus useful for e.g. getting around simple geo-blocks, or having webpages return
423
    /// the correct localised versions).
424
    ///
425
    /// Returning `None` signifies that no country code information is available. (Conflicting
426
    /// GeoIP lookup results might also cause `None` to be returned.)
427
    fn country_code(&self) -> Option<CountryCode>;
428
}
429

            
430
/// An export of a GeoIp database in a raw format suitable for embedding.
431
///
432
/// This format is deliberately undocumented, and not for other uses.
433
#[cfg(feature = "export")]
434
#[allow(clippy::exhaustive_structs, missing_docs)]
435
pub struct RawGeoipDbExport<'a> {
436
    pub ipv4_starts: &'a [u32],
437
    pub ipv4_ccs: &'a [Option<CountryCode>],
438
    pub ipv4_asns: Option<&'a [Option<NonZeroU32>]>,
439
    pub ipv6_starts: &'a [u128],
440
    pub ipv6_ccs: &'a [Option<CountryCode>],
441
    pub ipv6_asns: Option<&'a [Option<NonZeroU32>]>,
442
}
443

            
444
#[cfg(feature = "export")]
445
impl<'a> RawGeoipDbExport<'a> {
446
    /// Save the contents of this export into a set of data files in "Path".
447
    pub fn save(&self, path: &std::path::Path) -> std::io::Result<()> {
448
        use std::fs::write;
449
        fn into_bytes<'a, T>(data: &'a [T]) -> &'a [u8] {
450
            // SAFETY: Every possible bit sequence is a valid u8.
451
            let (pre, data, post) = unsafe { data.align_to::<u8>() };
452
            assert!(pre.is_empty());
453
            assert!(post.is_empty());
454
            data
455
        }
456
        write(path.join("geoip_data_v4s"), into_bytes(self.ipv4_starts))?;
457
        write(path.join("geoip_data_v4c"), into_bytes(self.ipv4_ccs))?;
458
        if let Some(asns) = self.ipv4_asns {
459
            write(path.join("geoip_data_v4a"), into_bytes(asns))?;
460
        }
461
        write(path.join("geoip_data_v6s"), into_bytes(self.ipv6_starts))?;
462
        write(path.join("geoip_data_v6c"), into_bytes(self.ipv6_ccs))?;
463
        if let Some(asns) = self.ipv6_asns {
464
            write(path.join("geoip_data_v6a"), into_bytes(asns))?;
465
        }
466
        Ok(())
467
    }
468
}
469

            
470
#[cfg(test)]
471
mod test {
472
    // @@ begin test lint list maintained by maint/add_warning @@
473
    #![allow(clippy::bool_assert_comparison)]
474
    #![allow(clippy::clone_on_copy)]
475
    #![allow(clippy::dbg_macro)]
476
    #![allow(clippy::mixed_attributes_style)]
477
    #![allow(clippy::print_stderr)]
478
    #![allow(clippy::print_stdout)]
479
    #![allow(clippy::single_char_pattern)]
480
    #![allow(clippy::unwrap_used)]
481
    #![allow(clippy::unchecked_time_subtraction)]
482
    #![allow(clippy::useless_vec)]
483
    #![allow(clippy::needless_pass_by_value)]
484
    #![allow(clippy::string_slice)] // See arti#2571
485
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
486

            
487
    use super::*;
488
    use std::net::Ipv4Addr;
489

            
490
    // NOTE(eta): this test takes a whole 1.6 seconds in *non-release* mode
491
    #[test]
492
    #[cfg(feature = "embedded-db")]
493
    fn embedded_db() {
494
        let db = GeoipDb::new_embedded();
495

            
496
        assert_eq!(
497
            db.lookup_country_code(Ipv4Addr::new(8, 8, 8, 8).into())
498
                .map(|x| x.as_ref()),
499
            Some("US")
500
        );
501

            
502
        assert_eq!(
503
            db.lookup_country_code("2001:4860:4860::8888".parse().unwrap())
504
                .map(|x| x.as_ref()),
505
            Some("US")
506
        );
507
    }
508

            
509
    #[test]
510
    fn cc_rep() {
511
        let italy = CountryCode::new("IT").unwrap();
512
        assert_eq!(italy.as_ref(), "IT");
513
    }
514

            
515
    #[test]
516
    fn basic_lookups() {
517
        let src_v4 = r#"
518
        16909056,16909311,GB
519
        "#;
520
        let src_v6 = r#"
521
        dead:beef::,dead:ffff::,??
522
        fe80::,fe81::,US
523
        "#;
524
        let db = GeoipDb::new_from_legacy_format(src_v4, src_v6, true).unwrap();
525

            
526
        assert_eq!(
527
            db.lookup_country_code(Ipv4Addr::new(1, 2, 3, 4).into())
528
                .map(|x| x.as_ref()),
529
            Some("GB")
530
        );
531

            
532
        assert_eq!(
533
            db.lookup_country_code(Ipv4Addr::new(1, 1, 1, 1).into()),
534
            None
535
        );
536

            
537
        assert_eq!(
538
            db.lookup_country_code("fe80::dead:beef".parse().unwrap())
539
                .map(|x| x.as_ref()),
540
            Some("US")
541
        );
542

            
543
        assert_eq!(
544
            db.lookup_country_code("fe81::dead:beef".parse().unwrap()),
545
            None
546
        );
547
        assert_eq!(
548
            db.lookup_country_code("dead:beef::1".parse().unwrap()),
549
            None
550
        );
551
    }
552

            
553
    #[test]
554
    fn cc_parse() -> Result<(), Error> {
555
        // real countries.
556
        assert_eq!(CountryCode::from_str("us")?, CountryCode::from_str("US")?);
557
        assert_eq!(CountryCode::from_str("UY")?, CountryCode::from_str("UY")?);
558

            
559
        // not real as of this writing, but still representable.
560
        assert_eq!(CountryCode::from_str("A7")?, CountryCode::from_str("a7")?);
561
        assert_eq!(CountryCode::from_str("xz")?, CountryCode::from_str("xz")?);
562

            
563
        // Can't convert to two bytes.
564
        assert!(matches!(
565
            CountryCode::from_str("z"),
566
            Err(Error::BadCountryCode(_))
567
        ));
568
        assert!(matches!(
569
            CountryCode::from_str("🐻‍❄️"),
570
            Err(Error::BadCountryCode(_))
571
        ));
572
        assert!(matches!(
573
            CountryCode::from_str("Sheboygan"),
574
            Err(Error::BadCountryCode(_))
575
        ));
576

            
577
        // Can convert to two bytes, but still not printable ascii
578
        assert!(matches!(
579
            CountryCode::from_str("\r\n"),
580
            Err(Error::BadCountryCode(_))
581
        ));
582
        assert!(matches!(
583
            CountryCode::from_str("\0\0"),
584
            Err(Error::BadCountryCode(_))
585
        ));
586
        assert!(matches!(
587
            CountryCode::from_str("¡"),
588
            Err(Error::BadCountryCode(_))
589
        ));
590

            
591
        // Not a country.
592
        assert!(matches!(
593
            CountryCode::from_str("??"),
594
            Err(Error::NowhereNotSupported)
595
        ));
596

            
597
        Ok(())
598
    }
599

            
600
    #[test]
601
    fn opt_cc_parse() -> Result<(), Error> {
602
        assert_eq!(
603
            CountryCode::from_str("br")?,
604
            OptionCc::from_str("BR")?.0.unwrap()
605
        );
606
        assert!(OptionCc::from_str("??")?.0.is_none());
607

            
608
        Ok(())
609
    }
610
}