From 665bc6d2de6f8c0857d8a11b88f0d132b510a49f Mon Sep 17 00:00:00 2001 From: Matei Radu Date: Fri, 5 Jul 2024 22:01:37 +0200 Subject: [PATCH] Add Domain struct, replace bytes_are_domain Since it's expected to do many operation on individual labels, a `Domain` representation is needed. And, since there's a struct now, the standalone `bytes_are_domain` function is replaced with a `try_from` trait implementation. --- Cargo.lock | 2 +- lib/Cargo.toml | 2 +- lib/src/domain.rs | 89 ++++++++++++++++++++++++++++++++++++++--------- 3 files changed, 75 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 52d9031..6029c46 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -25,7 +25,7 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "dns_lib" -version = "0.2.1" +version = "0.3.0" dependencies = [ "rstest", ] diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 7a0938b..bbdafd1 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dns_lib" -version = "0.2.1" +version = "0.3.0" description = "An implementation of the DNS protocol from scratch based on the many DNS RFCs." rust-version.workspace = true diff --git a/lib/src/domain.rs b/lib/src/domain.rs index ff87dc6..814e6b5 100644 --- a/lib/src/domain.rs +++ b/lib/src/domain.rs @@ -13,15 +13,70 @@ // limitations under the License. const MAX_LABEL_LENGTH: usize = 63; -const LABEL_SEPARATOR: u8 = b'.'; +const LABEL_SEPARATOR: char = '.'; -/// Checks if the byte array is a valid DNS `domain`, that is, a string -/// consisting of one of more `label`s separated by dots ("."). +#[derive(Debug)] +pub struct InvalidDomainError; + +/// Representation of a DNS domain name. /// -/// See [RFC 1034, Section 3.5 - Preferred name syntax](https://datatracker.ietf.org/doc/html/rfc1034#section-3.5) -pub fn bytes_are_domain(bytes: &[u8]) -> bool { - let labels: Vec<&[u8]> = bytes.split(|&byte| byte == LABEL_SEPARATOR).collect(); - labels.iter().all(|&label| bytes_are_label(label)) +/// A domain name consists of one or more labels. Each label starts with a +/// letter, ends with a letter or digit, and can contain letters, digits, +/// and hyphens in between. +/// +/// When represented as a string, each label is separated by dots (`.`): +/// +/// > `www.example.com` +/// +/// For more details, see [RFC 1034, Section 3.5]. +/// +/// [RFC 1034, Section 3.5]: https://datatracker.ietf.org/doc/html/rfc1034#section-3.5 +#[derive(Debug, PartialEq)] +pub struct Domain { + labels: Vec, +} + +impl TryFrom for Domain { + type Error = InvalidDomainError; + + /// Tries to convert a [`String`] into a `Domain`. + /// + /// A valid DNS domain name string consists of one or more labels separated + /// by dots (`.`). Each label starts with a letter, ends with a letter or + /// digit, and can contain letters, digits, and hyphens in between. + /// + /// For more details, see [RFC 1034, Section 3.5]. + /// + /// # Example + /// ``` + /// use dns_lib::domain::Domain; + /// + /// let valid_domain = "example.com".to_string(); + /// assert!(Domain::try_from(valid_domain).is_ok()); + /// + /// let invalid_domain = "foo-..bar".to_string(); + /// assert!(Domain::try_from(invalid_domain).is_err()); + /// ``` + /// + /// [RFC 1034, Section 3.5]: https://datatracker.ietf.org/doc/html/rfc1034#section-3.5 + fn try_from(value: String) -> Result { + let labels: Vec = value + .split(|character| character == LABEL_SEPARATOR) + .map(String::from) + .collect(); + + if labels.is_empty() { + return Err(InvalidDomainError); + } + + for label in &labels { + if !bytes_are_label(label.as_bytes()) { + return Err(InvalidDomainError); + } + } + + Ok(Domain { labels }) + } } /// Checks if the byte array is a valid DNS `label`, that is, a string that @@ -104,14 +159,16 @@ mod tests { } #[rstest] - #[case(b"a", true)] - #[case(b"example", true)] - #[case(b"example.com", true)] - #[case(b"mercedes-benz.de", true)] - #[case(b"live-365", true)] - #[case(b"live-365.com", true)] - #[case(b"d111111abcdef8.cloudfront.net", true)] - fn bytes_are_domain_works_correctly(#[case] input: &[u8], #[case] expected: bool) { - assert_eq!(bytes_are_domain(input), expected); + #[case("a", Domain{ labels: vec!["a".to_string()]})] + #[case("example", Domain{ labels: vec!["example".to_string()]})] + #[case("example.com", Domain{ labels: vec!["example".to_string(), "com".to_string()]})] + #[case("mercedes-benz.de", Domain{ labels: vec!["mercedes-benz".to_string(), "de".to_string()]})] + #[case("live-365", Domain{ labels: vec!["live-365".to_string()]})] + #[case("live-365.com", Domain{ labels: vec!["live-365".to_string(), "com".to_string()]})] + #[case("d111111abcdef8.cloudfront.net", Domain{ labels: vec!["d111111abcdef8".to_string(), "cloudfront".to_string(), "net".to_string()]})] + fn domain_try_from_succeeds(#[case] input: String, #[case] ok: Domain) { + let result = Domain::try_from(input); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), ok); } }