├── .gitignore ├── fuzz ├── .gitignore ├── fuzz_targets │ ├── domain.rs │ └── suffix.rs └── Cargo.toml ├── benches └── benches.rs ├── Cargo.toml ├── .github └── workflows │ ├── ci.yml │ └── update.yaml ├── LICENSE ├── Makefile.toml ├── src └── lib.rs ├── examples └── tldextract.rs ├── tests ├── tests.txt └── list.rs └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | *.swp 4 | -------------------------------------------------------------------------------- /fuzz/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | target 3 | corpus 4 | artifacts 5 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/domain.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | 3 | use libfuzzer_sys::fuzz_target; 4 | use psl::{Psl, List}; 5 | 6 | fuzz_target!(|data: &[u8]| { 7 | List.domain(data); 8 | }); 9 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/suffix.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | 3 | use libfuzzer_sys::fuzz_target; 4 | use psl::{Psl, List}; 5 | 6 | fuzz_target!(|data: &[u8]| { 7 | List.suffix(data); 8 | }); 9 | -------------------------------------------------------------------------------- /benches/benches.rs: -------------------------------------------------------------------------------- 1 | #![feature(test)] 2 | 3 | extern crate test; 4 | 5 | use psl::{List, Psl}; 6 | use test::Bencher; 7 | 8 | const DOMAIN: &[u8] = b"www.example.com"; 9 | 10 | #[bench] 11 | fn bench_find(b: &mut Bencher) { 12 | b.iter(|| List.find(DOMAIN.rsplit(|x| *x == b'.'))); 13 | } 14 | 15 | #[bench] 16 | fn bench_suffix(b: &mut Bencher) { 17 | b.iter(|| List.suffix(DOMAIN).unwrap()); 18 | } 19 | 20 | #[bench] 21 | fn bench_domain(b: &mut Bencher) { 22 | b.iter(|| List.domain(DOMAIN).unwrap()); 23 | } 24 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "psl" 3 | description = "Extract root domain and suffix from a domain name" 4 | version = "2.1.173" 5 | license = "MIT/Apache-2.0" 6 | repository = "https://github.com/addr-rs/psl" 7 | documentation = "https://docs.rs/psl" 8 | readme = "README.md" 9 | keywords = ["tld", "no_std", "tldextract", "domain", "publicsuffix"] 10 | authors = ["rushmorem "] 11 | edition = "2018" 12 | exclude = ["/tests"] 13 | 14 | [features] 15 | default = ["helpers"] 16 | helpers = [ ] 17 | 18 | [dependencies] 19 | psl-types = "2.0.11" 20 | 21 | [dev-dependencies] 22 | rspec = "1.0.0" 23 | -------------------------------------------------------------------------------- /fuzz/Cargo.toml: -------------------------------------------------------------------------------- 1 | 2 | [package] 3 | name = "psl-fuzz" 4 | version = "0.0.1" 5 | authors = ["Automatically generated"] 6 | publish = false 7 | edition = "2018" 8 | 9 | [package.metadata] 10 | cargo-fuzz = true 11 | 12 | [dependencies] 13 | libfuzzer-sys = "0.4" 14 | 15 | [dependencies.psl] 16 | path = ".." 17 | 18 | # Prevent this from interfering with workspaces 19 | [workspace] 20 | members = ["."] 21 | 22 | [[bin]] 23 | name = "suffix" 24 | path = "fuzz_targets/suffix.rs" 25 | test = false 26 | doc = false 27 | 28 | [[bin]] 29 | name = "domain" 30 | path = "fuzz_targets/domain.rs" 31 | test = false 32 | doc = false 33 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | check: 7 | name: Check on v1.41 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout sources 11 | uses: actions/checkout@v4 12 | 13 | - name: Install stable toolchain 14 | uses: dtolnay/rust-toolchain@stable 15 | with: 16 | toolchain: 1.41.1 17 | 18 | - name: Run cargo check 19 | run: cargo check --all-features 20 | 21 | test: 22 | name: Test on stable 23 | runs-on: ubuntu-latest 24 | steps: 25 | - name: Checkout sources 26 | uses: actions/checkout@v4 27 | 28 | - name: Install stable toolchain 29 | uses: dtolnay/rust-toolchain@stable 30 | with: 31 | toolchain: stable 32 | components: rustfmt, clippy 33 | 34 | - name: Run cargo fmt 35 | run: cargo fmt --all -- --check 36 | 37 | - name: Run cargo clippy 38 | run: cargo clippy --all-features -- -D warnings 39 | 40 | - name: Run cargo test 41 | run: cargo test --all-features 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Rushmore Mushambi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile.toml: -------------------------------------------------------------------------------- 1 | [tasks.compile-list] 2 | env = { "CARGO_MAKE_RUST_SCRIPT_PROVIDER" = "cargo-play" } 3 | script_runner = "@rust" 4 | script = ''' 5 | //# psl-codegen = "0.9" 6 | 7 | fn main() { 8 | let list = psl_codegen::compile_psl("data/rules.txt"); 9 | 10 | let module = format!("//! This file is automatically @generated by cargo-make. 11 | //! It is not intended for manual editing. 12 | 13 | #![allow(clippy::all)] // TODO lint this code? 14 | 15 | {}", list); 16 | 17 | std::fs::write("src/list.rs", module).unwrap(); 18 | } 19 | ''' 20 | 21 | [tasks.format] 22 | install_crate = "rustfmt" 23 | command = "cargo" 24 | args = ["fmt", "--all"] 25 | dependencies = ["compile-list"] 26 | 27 | [tasks.check] 28 | command = "cargo" 29 | args = ["check", "--all-features"] 30 | dependencies = ["format"] 31 | 32 | [tasks.build] 33 | command = "cargo" 34 | args = ["build", "--all-features"] 35 | dependencies = ["format"] 36 | 37 | [tasks.clippy] 38 | command = "cargo" 39 | args = ["clippy", "--all-features"] 40 | dependencies = ["format"] 41 | 42 | [tasks.test] 43 | command = "cargo" 44 | args = ["test", "--all-features"] 45 | dependencies = ["format"] 46 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! A native Rust library for Mozilla's Public Suffix List 2 | 3 | #![no_std] 4 | #![forbid(unsafe_code)] 5 | 6 | mod list; 7 | 8 | #[cfg(feature = "helpers")] 9 | use core::str; 10 | 11 | pub use psl_types::{Domain, Info, List as Psl, Suffix, Type}; 12 | 13 | /// A static public suffix list 14 | pub struct List; 15 | 16 | impl Psl for List { 17 | #[inline] 18 | fn find<'a, T>(&self, labels: T) -> Info 19 | where 20 | T: Iterator, 21 | { 22 | list::lookup(labels) 23 | } 24 | } 25 | 26 | /// Get the public suffix of the domain 27 | #[cfg(feature = "helpers")] 28 | #[inline] 29 | pub fn suffix(name: &[u8]) -> Option> { 30 | List.suffix(name) 31 | } 32 | 33 | /// Get the public suffix of the domain 34 | #[cfg(feature = "helpers")] 35 | #[inline] 36 | pub fn suffix_str(name: &str) -> Option<&str> { 37 | let bytes = suffix(name.as_bytes())?.trim().as_bytes(); 38 | str::from_utf8(bytes).ok() 39 | } 40 | 41 | /// Get the registrable domain 42 | #[cfg(feature = "helpers")] 43 | #[inline] 44 | pub fn domain(name: &[u8]) -> Option> { 45 | List.domain(name) 46 | } 47 | 48 | /// Get the registrable domain 49 | #[cfg(feature = "helpers")] 50 | #[inline] 51 | pub fn domain_str(name: &str) -> Option<&str> { 52 | let bytes = domain(name.as_bytes())?.trim().as_bytes(); 53 | str::from_utf8(bytes).ok() 54 | } 55 | -------------------------------------------------------------------------------- /.github/workflows/update.yaml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main 8 | schedule: 9 | - cron: '0 0 * * *' # midnight UTC 10 | 11 | env: 12 | CARGO_TERM_COLOR: always 13 | 14 | jobs: 15 | publish: 16 | 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - name: Checkout repository 21 | uses: actions/checkout@v4 22 | 23 | - name: Install Rust toolchain 24 | uses: dtolnay/rust-toolchain@stable 25 | with: 26 | toolchain: stable 27 | components: rustfmt 28 | 29 | - name: Install deps 30 | run: | 31 | sudo apt update 32 | sudo apt install --yes gcc pkg-config libssl-dev 33 | 34 | - name: Install cargo play 35 | run: cargo install --locked cargo-play 36 | 37 | - name: Install cargo make 38 | run: cargo install --locked cargo-make 39 | 40 | - name: Download the list and run tests 41 | run: cargo make test 42 | 43 | - name: Publish 44 | env: 45 | CARGO_REGISTRY_TOKEN: ${{ secrets.CRATES_TOKEN }} 46 | GIT_TOKEN: ${{ secrets.GITHUB_TOKEN }} 47 | shell: bash 48 | run: | 49 | if ! git diff --exit-code --quiet data/rules.txt; then 50 | cargo doc --all-features 51 | git config --local user.email "actions@users.noreply.github.com" 52 | git config --local user.name "github-actions[bot]" 53 | git remote add upstream https://rushmorem:${{ secrets.GITHUB_TOKEN }}@github.com/addr-rs/psl.git 54 | git add data/rules.txt src/list.rs 55 | git commit -m 'update the list' 56 | cargo install --force --locked --version 0.3.108 release-plz 57 | /home/runner/.cargo/bin/release-plz update --no-changelog 58 | git add Cargo.toml 59 | git commit -m 'bump version' 60 | git push upstream main 61 | /home/runner/.cargo/bin/release-plz release 62 | fi 63 | -------------------------------------------------------------------------------- /examples/tldextract.rs: -------------------------------------------------------------------------------- 1 | use psl_types::List; 2 | 3 | pub trait TldExtract { 4 | fn extract<'a>(&self, host: &'a str) -> Option>; 5 | } 6 | 7 | impl TldExtract for T { 8 | fn extract<'a>(&self, host: &'a str) -> Option> { 9 | let host_len = host.len(); 10 | let suffix_len = self.suffix(host.as_bytes())?.as_bytes().len(); 11 | let suffix = { 12 | let offset = host_len - suffix_len; 13 | &host[offset..] 14 | }; 15 | let suffix_plus_dot = suffix_len + 1; 16 | let (subdomain, domain) = if host_len > suffix_plus_dot { 17 | match host.get(..host_len - suffix_plus_dot) { 18 | Some(prefix) => match prefix.rfind('.') { 19 | Some(offset) => (prefix.get(..offset), prefix.get(offset + 1..)), 20 | None => (None, Some(prefix)), 21 | }, 22 | None => (None, None), 23 | } 24 | } else { 25 | (None, None) 26 | }; 27 | Some(Parts { 28 | suffix, 29 | domain, 30 | subdomain, 31 | }) 32 | } 33 | } 34 | 35 | #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)] 36 | pub struct Parts<'a> { 37 | pub suffix: &'a str, 38 | pub domain: Option<&'a str>, 39 | pub subdomain: Option<&'a str>, 40 | } 41 | 42 | // This example is inspired by https://github.com/john-kurkowski/tldextract 43 | // Unlike that project, we don't try to parse URLs though. That can easily 44 | // be done by using the `url` crate and feeding the output of `Url::domain` 45 | // to `TldExtract::extract`. 46 | fn main() { 47 | use psl::List; 48 | use std::env; 49 | 50 | let domain = match env::args().nth(1) { 51 | Some(name) => name, 52 | None => { 53 | eprintln!("Usage: {} ", env::args().nth(0).unwrap()); 54 | std::process::exit(1); 55 | } 56 | }; 57 | 58 | match List.extract(&domain) { 59 | Some(info) => println!( 60 | "{} {} {}", 61 | info.subdomain.unwrap_or("(None)"), 62 | info.domain.unwrap_or("(None)"), 63 | info.suffix 64 | ), 65 | None => eprintln!("`{}` is not domain name", domain), 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /tests/tests.txt: -------------------------------------------------------------------------------- 1 | // Any copyright is dedicated to the Public Domain. 2 | // https://creativecommons.org/publicdomain/zero/1.0/ 3 | 4 | // null input. 5 | null null 6 | // Mixed case. 7 | COM null 8 | example.COM example.com 9 | WwW.example.COM example.com 10 | // Leading dot. 11 | .com null 12 | .example null 13 | .example.com null 14 | .example.example null 15 | // Unlisted TLD. 16 | example null 17 | example.example example.example 18 | b.example.example example.example 19 | a.b.example.example example.example 20 | // Listed, but non-Internet, TLD. 21 | //local null 22 | //example.local null 23 | //b.example.local null 24 | //a.b.example.local null 25 | // TLD with only 1 rule. 26 | biz null 27 | domain.biz domain.biz 28 | b.domain.biz domain.biz 29 | a.b.domain.biz domain.biz 30 | // TLD with some 2-level rules. 31 | com null 32 | example.com example.com 33 | b.example.com example.com 34 | a.b.example.com example.com 35 | uk.com null 36 | example.uk.com example.uk.com 37 | b.example.uk.com example.uk.com 38 | a.b.example.uk.com example.uk.com 39 | test.ac test.ac 40 | // TLD with only 1 (wildcard) rule. 41 | mm null 42 | c.mm null 43 | b.c.mm b.c.mm 44 | a.b.c.mm b.c.mm 45 | // More complex TLD. 46 | jp null 47 | test.jp test.jp 48 | www.test.jp test.jp 49 | ac.jp null 50 | test.ac.jp test.ac.jp 51 | www.test.ac.jp test.ac.jp 52 | kyoto.jp null 53 | test.kyoto.jp test.kyoto.jp 54 | ide.kyoto.jp null 55 | b.ide.kyoto.jp b.ide.kyoto.jp 56 | a.b.ide.kyoto.jp b.ide.kyoto.jp 57 | c.kobe.jp null 58 | b.c.kobe.jp b.c.kobe.jp 59 | a.b.c.kobe.jp b.c.kobe.jp 60 | city.kobe.jp city.kobe.jp 61 | www.city.kobe.jp city.kobe.jp 62 | // TLD with a wildcard rule and exceptions. 63 | ck null 64 | test.ck null 65 | b.test.ck b.test.ck 66 | a.b.test.ck b.test.ck 67 | www.ck www.ck 68 | www.www.ck www.ck 69 | // US K12. 70 | us null 71 | test.us test.us 72 | www.test.us test.us 73 | ak.us null 74 | test.ak.us test.ak.us 75 | www.test.ak.us test.ak.us 76 | k12.ak.us null 77 | test.k12.ak.us test.k12.ak.us 78 | www.test.k12.ak.us test.k12.ak.us 79 | // IDN labels. 80 | 食狮.com.cn 食狮.com.cn 81 | 食狮.公司.cn 食狮.公司.cn 82 | www.食狮.公司.cn 食狮.公司.cn 83 | shishi.公司.cn shishi.公司.cn 84 | 公司.cn null 85 | 食狮.中国 食狮.中国 86 | www.食狮.中国 食狮.中国 87 | shishi.中国 shishi.中国 88 | 中国 null 89 | // Same as above, but punycoded. 90 | xn--85x722f.com.cn xn--85x722f.com.cn 91 | xn--85x722f.xn--55qx5d.cn xn--85x722f.xn--55qx5d.cn 92 | www.xn--85x722f.xn--55qx5d.cn xn--85x722f.xn--55qx5d.cn 93 | shishi.xn--55qx5d.cn shishi.xn--55qx5d.cn 94 | xn--55qx5d.cn null 95 | xn--85x722f.xn--fiqs8s xn--85x722f.xn--fiqs8s 96 | www.xn--85x722f.xn--fiqs8s xn--85x722f.xn--fiqs8s 97 | shishi.xn--fiqs8s shishi.xn--fiqs8s 98 | xn--fiqs8s null 99 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PSL 2 | 3 | A native Rust library for Mozilla's Public Suffix List 4 | 5 | [![CI](https://github.com/addr-rs/psl/actions/workflows/ci.yml/badge.svg)](https://github.com/addr-rs/psl/actions/workflows/ci.yml) 6 | [![Publish](https://github.com/addr-rs/psl/actions/workflows/update.yaml/badge.svg)](https://github.com/addr-rs/psl/actions/workflows/update.yaml) 7 | [![Latest Version](https://img.shields.io/crates/v/psl.svg)](https://crates.io/crates/psl) 8 | [![Crates.io downloads](https://img.shields.io/crates/d/psl)](https://crates.io/crates/psl) 9 | [![Docs](https://docs.rs/psl/badge.svg)](https://docs.rs/psl) 10 | [![Minimum supported Rust version](https://img.shields.io/badge/rustc-1.41+-yellow.svg)](https://www.rust-lang.org) 11 | ![Maintenance](https://img.shields.io/badge/maintenance-actively--developed-brightgreen.svg) 12 | [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) 13 | 14 | This library uses Mozilla's [Public Suffix List](https://publicsuffix.org) to reliably determine the suffix of a domain name. 15 | 16 | It compiles the list down to native Rust code for ultimate speed. This list compilation is done as a separate step by the `Publish` GitHub Action so the crate still compiles very quickly. The `Publish` action automatically checks for updates everyday and pushes an updated crate to crates.io if there were any updates in the upstream domain suffixes. This keeps the crate automatically synchronised with the official list. 17 | 18 | If you need a dynamic list that can be updated at runtime, though a bit slower, please use the [publicsuffix](https://crates.io/crates/publicsuffix) crate instead (which also has optional support for looking up domain names in any case). 19 | 20 | ## Setting Up 21 | 22 | Add this crate to your `Cargo.toml`: 23 | 24 | ```toml 25 | [dependencies] 26 | psl = "2" 27 | ``` 28 | 29 | ## Examples 30 | 31 | ```rust 32 | let suffix = psl::suffix(b"www.example.com")?; 33 | assert_eq!(suffix, "com"); 34 | assert_eq!(suffix.typ(), Some(psl::Type::Icann)); 35 | 36 | let domain = psl::domain(b"www.example.com")?; 37 | assert_eq!(domain, "example.com"); 38 | assert_eq!(domain.suffix(), "com"); 39 | 40 | let domain = psl::domain("www.食狮.中国".as_bytes())?; 41 | assert_eq!(domain, "食狮.中国"); 42 | assert_eq!(domain.suffix(), "中国"); 43 | 44 | let domain = psl::domain(b"www.xn--85x722f.xn--55qx5d.cn")?; 45 | assert_eq!(domain, "xn--85x722f.xn--55qx5d.cn"); 46 | assert_eq!(domain.suffix(), "xn--55qx5d.cn"); 47 | 48 | let domain = psl::domain(b"a.b.example.uk.com")?; 49 | assert_eq!(domain, "example.uk.com"); 50 | assert_eq!(domain.suffix(), "uk.com"); 51 | 52 | let domain = psl::domain(b"_tcp.example.com.")?; 53 | assert_eq!(domain, "example.com."); 54 | assert_eq!(domain.suffix(), "com."); 55 | ``` 56 | -------------------------------------------------------------------------------- /tests/list.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | use std::io::prelude::*; 3 | use std::path::Path; 4 | use std::{env, str}; 5 | 6 | use psl::{List, Psl, Type}; 7 | use rspec::report::ExampleResult; 8 | 9 | #[test] 10 | fn list_behaviour() { 11 | rspec::run(&rspec::describe("the official test", (), |ctx| { 12 | let body = { 13 | let root = env::var("CARGO_MANIFEST_DIR").unwrap(); 14 | let path = Path::new(&root).join("tests").join("tests.txt"); 15 | let mut file = File::open(path).unwrap(); 16 | let mut contents = String::new(); 17 | file.read_to_string(&mut contents).unwrap(); 18 | contents 19 | }; 20 | 21 | let mut parse = false; 22 | 23 | for (i, line) in body.lines().enumerate() { 24 | match line { 25 | line if line.trim().is_empty() => { 26 | parse = true; 27 | continue; 28 | } 29 | line if line.starts_with("//") => { 30 | continue; 31 | } 32 | line => { 33 | if !parse { 34 | continue; 35 | } 36 | let mut test = line.split_whitespace().peekable(); 37 | if test.peek().is_none() { 38 | continue; 39 | } 40 | let input = match test.next() { 41 | Some("null") => "", 42 | Some(res) => res, 43 | None => { 44 | panic!("line {} of the test file doesn't seem to be valid", i); 45 | } 46 | }; 47 | if !expected_tld(input) { 48 | continue; 49 | } 50 | let (expected_root, expected_suffix) = match test.next() { 51 | Some("null") => (None, None), 52 | Some(root) => { 53 | let suffix = { 54 | let parts: Vec<&str> = root.split('.').rev().collect(); 55 | (&parts[..parts.len() - 1]) 56 | .iter() 57 | .rev() 58 | .map(|part| *part) 59 | .collect::>() 60 | .join(".") 61 | }; 62 | (Some(root.to_string()), Some(suffix.to_string())) 63 | } 64 | None => { 65 | panic!("line {} of the test file doesn't seem to be valid", i); 66 | } 67 | }; 68 | let (found_root, found_suffix) = 69 | if input.starts_with(".") || input.contains("..") { 70 | (None, None) 71 | } else { 72 | List.domain(input.to_lowercase().as_bytes()) 73 | .map(|d| { 74 | let domain = str::from_utf8(d.as_bytes()).unwrap().to_string(); 75 | let suffix = 76 | str::from_utf8(d.suffix().as_bytes()).unwrap().to_string(); 77 | (Some(domain), Some(suffix)) 78 | }) 79 | .unwrap_or((None, None)) 80 | }; 81 | ctx.when(msg(format!("input is `{}`", input)), |ctx| { 82 | let full_domain = expected_root.is_some(); 83 | 84 | ctx.it(msg(format!("means the root domain {}", val(&expected_root))), move |_| { 85 | if expected_root == found_root { 86 | ExampleResult::Success 87 | } else { 88 | let msg = format!("expected `{:?}` but found `{:?}` on line {} of `test_psl.txt`", expected_root, found_root, i+1); 89 | ExampleResult::Failure(Some(msg)) 90 | } 91 | }); 92 | 93 | if full_domain { 94 | ctx.it(msg(format!("also means the suffix {}", val(&expected_suffix))), move |_| { 95 | if expected_suffix == found_suffix { 96 | ExampleResult::Success 97 | } else { 98 | let msg = format!("expected `{:?}` but found `{:?}` on line {} of `test_psl.txt`", expected_suffix, found_suffix, i+1); 99 | ExampleResult::Failure(Some(msg)) 100 | } 101 | }); 102 | } 103 | }); 104 | } 105 | } 106 | } 107 | })); 108 | 109 | rspec::run(&rspec::describe("suffix tests", (), |ctx| { 110 | let extra = vec![ 111 | ( 112 | "gp-id-ter-acc-1.to.gp-kl-cas-11-ses001-ses-1.wdsl.5m.za", 113 | "za", 114 | ), 115 | ("yokohama.jp", "jp"), 116 | ("kobe.jp", "jp"), 117 | ("foo.bar.platformsh.site", "bar.platformsh.site"), 118 | ("bar.platformsh.site", "bar.platformsh.site"), 119 | ("platform.sh", "sh"), 120 | ("sh", "sh"), 121 | (".", "."), 122 | ("example.com.", "com."), 123 | ("www.食狮.中国", "中国"), 124 | ("www.xn--85x722f.xn--55qx5d.cn", "xn--55qx5d.cn"), 125 | ("a.b.example.uk.com", "uk.com"), 126 | ("_tcp.example.com.", "com."), 127 | ]; 128 | 129 | for (input, expected) in extra { 130 | if !expected_tld(input) { 131 | continue; 132 | } 133 | ctx.when(msg(format!("input is `{}`", input)), |ctx| { 134 | let expected_suffix = Some(expected); 135 | ctx.it( 136 | msg(format!( 137 | "means the suffix {}", 138 | val(&expected_suffix.map(ToString::to_string)) 139 | )), 140 | move |_| { 141 | let suffix = List.suffix(input.as_bytes()).unwrap(); 142 | if suffix == expected { 143 | ExampleResult::Success 144 | } else { 145 | let msg = format!( 146 | "expected `{:?}` but found `{:?}`", 147 | expected_suffix, 148 | Some(str::from_utf8(suffix.as_bytes()).unwrap().to_string()) 149 | ); 150 | ExampleResult::Failure(Some(msg)) 151 | } 152 | }, 153 | ); 154 | }); 155 | } 156 | })); 157 | 158 | rspec::run(&rspec::describe("suffix type tests", (), |ctx| { 159 | let extra = vec![ 160 | ( 161 | "gp-id-ter-acc-1.to.gp-kl-cas-11-ses001-ses-1.wdsl.5m.za", 162 | false, 163 | None, 164 | ), 165 | ("yokohama.jp", true, Some(Type::Icann)), 166 | ("kobe.jp", true, Some(Type::Icann)), 167 | ("foo.bar.platformsh.site", true, Some(Type::Private)), 168 | ("bar.platformsh.site", true, Some(Type::Private)), 169 | ("platform.sh", true, Some(Type::Icann)), 170 | ("sh", true, Some(Type::Icann)), 171 | (".", false, None), 172 | ("example.gafregsrse", false, None), 173 | ("www.食狮.中国", true, Some(Type::Icann)), 174 | ("www.xn--85x722f.xn--55qx5d.cn", true, Some(Type::Icann)), 175 | ]; 176 | 177 | for (input, known_suffix, typ) in extra { 178 | if !expected_tld(input) { 179 | continue; 180 | } 181 | ctx.when(msg(format!("input is `{}`", input)), |ctx| { 182 | ctx.it( 183 | msg(format!( 184 | "means known suffix {}", 185 | val(&Some(known_suffix.to_string())) 186 | )), 187 | move |_| { 188 | let suffix = List.suffix(input.as_bytes()).unwrap(); 189 | assert_eq!(suffix.typ(), typ); 190 | if suffix.is_known() == known_suffix { 191 | ExampleResult::Success 192 | } else { 193 | let msg = format!( 194 | "expected `{:?}` but found `{:?}`", 195 | known_suffix, 196 | suffix.is_known() 197 | ); 198 | ExampleResult::Failure(Some(msg)) 199 | } 200 | }, 201 | ); 202 | }); 203 | } 204 | })); 205 | } 206 | 207 | // Converts a String to &'static str 208 | // 209 | // This will leak memory but that's OK for our testing purposes 210 | fn msg(s: String) -> &'static str { 211 | Box::leak(s.into_boxed_str()) 212 | } 213 | 214 | fn val(s: &Option) -> String { 215 | match *s { 216 | Some(ref v) => format!("should be `{}`", v), 217 | None => format!("is invalid"), 218 | } 219 | } 220 | 221 | fn expected_tld(input: &str) -> bool { 222 | let var = if let Ok(var) = env::var("PSL_TLD") { 223 | var 224 | } else { 225 | String::new() 226 | }; 227 | var.trim().is_empty() || input.trim().trim_end_matches('.').ends_with(&var) 228 | } 229 | --------------------------------------------------------------------------------