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
#![warn(clippy::or_fun_call)]
28
#![deny(clippy::print_stderr)]
29
#![deny(clippy::print_stdout)]
30
#![warn(clippy::rc_buffer)]
31
#![deny(clippy::ref_option_ref)]
32
#![warn(clippy::semicolon_if_nothing_returned)]
33
#![warn(clippy::trait_duplication_in_bounds)]
34
#![deny(clippy::unchecked_time_subtraction)]
35
#![deny(clippy::unnecessary_wraps)]
36
#![warn(clippy::unseparated_literal_suffix)]
37
#![deny(clippy::unwrap_used)]
38
#![deny(clippy::mod_module_files)]
39
#![allow(clippy::let_unit_value)] // This can reasonably be done for explicitness
40
#![allow(clippy::uninlined_format_args)]
41
#![allow(clippy::significant_drop_in_scrutinee)] // arti/-/merge_requests/588/#note_2812945
42
#![allow(clippy::result_large_err)] // temporary workaround for arti#587
43
#![allow(clippy::needless_raw_string_hashes)] // complained-about code is fine, often best
44
#![allow(clippy::needless_lifetimes)] // See arti#1765
45
#![allow(mismatched_lifetime_syntaxes)] // temporary workaround for arti#2060
46
#![allow(clippy::collapsible_if)] // See arti#2342
47
#![deny(clippy::unused_async)]
48
#![deny(clippy::string_slice)] // See arti#2571
49
#![allow(recursion_depth_exceeding_limit)] // arti#2715, rust/issues/159228
50
//! <!-- @@ end lint list maintained by maint/add_warning @@ -->
51

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

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

            
64
mod dense_range_map;
65
mod err;
66

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

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

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

            
115
166
        let cc = cc_orig.to_ascii_uppercase();
116

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

            
122
323
        if !cc.iter().all(|b| b.is_ascii() && !b.is_ascii_control()) {
123
3
            return Err(Error::BadCountryCode(cc_orig.to_owned()));
124
160
        }
125

            
126
160
        if &cc == b"??" {
127
3
            return Err(Error::NowhereNotSupported);
128
157
        }
129

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            
281
49
        Ok(Self { map_v4, map_v6 })
282
49
    }
283

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

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

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

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

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

            
331
4
                ret = Some(cc);
332
980
            }
333
        }
334

            
335
697
        ret
336
698
    }
337

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

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

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

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

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

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

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

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

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

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

            
413
147
    Ok(Some((from..=to, cc, asn)))
414
247
}
415

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            
598
        Ok(())
599
    }
600

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

            
609
        Ok(())
610
    }
611
}