Skip to content

Commit

Permalink
Refactor Domain errors
Browse files Browse the repository at this point in the history
Generally, improved Domain related errors to be more detailed.
  • Loading branch information
matei-radu committed Jul 9, 2024
1 parent 61d9086 commit 9dddc06
Show file tree
Hide file tree
Showing 6 changed files with 104 additions and 50 deletions.
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion lib/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "dns_lib"
version = "0.8.0"
version = "0.9.0"
description = "An implementation of the DNS protocol from scratch based on the many DNS RFCs."

rust-version.workspace = true
Expand Down
55 changes: 55 additions & 0 deletions lib/src/domain/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Copyright 2024 Matei Bogdan Radu
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use crate::domain;
use std::error::Error;
use std::fmt;
use std::string::FromUtf8Error;

#[derive(Debug, PartialEq)]
pub enum TryFromError {
DomainEmpty,
LabelEmpty,
LabelTooLong(String),
LabelInvalidEncoding(FromUtf8Error),
LabelInvalidFormat(String),
}

impl fmt::Display for TryFromError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::DomainEmpty => write!(f, "domain is empty"),
Self::LabelEmpty => write!(f, "label is empty"),
Self::LabelTooLong(msg) => write!(
f,
"label '{}' exceeds the maximum allowed length of {} characters",
msg,
domain::name::MAX_LABEL_LENGTH
),
Self::LabelInvalidFormat(msg) => write!(f, "label '{}' has invalid format", msg),
Self::LabelInvalidEncoding(err) => {
write!(f, "label has invalid encoding format: {}", err)
}
}
}
}

impl Error for TryFromError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::LabelInvalidEncoding(err) => Some(err),
_ => None,
}
}
}
19 changes: 19 additions & 0 deletions lib/src/domain/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Copyright 2024 Matei Bogdan Radu
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

mod error;
mod name;

pub use error::TryFromError;
pub use name::Domain;
74 changes: 26 additions & 48 deletions lib/src/domain.rs → lib/src/domain/name.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,34 +12,12 @@
// See the License for the specific language governing permissions and
// limitations under the License.

use crate::domain::error::TryFromError;
use std::fmt;

const MAX_LABEL_LENGTH: usize = 63;
pub const MAX_LABEL_LENGTH: usize = 63;
const LABEL_SEPARATOR: char = '.';

#[derive(Debug)]
pub enum DomainError {
EmptyDomain,
EmptyLabel,
LabelTooLong,
InvalidLabelFormat,
}

impl fmt::Display for DomainError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::EmptyDomain => write!(f, "domain is empty"),
Self::EmptyLabel => write!(f, "label is empty"),
Self::LabelTooLong => write!(
f,
"label exceeds the maximum allowed length of {} characters",
MAX_LABEL_LENGTH
),
Self::InvalidLabelFormat => write!(f, "label is invalid"),
}
}
}

/// Representation of a DNS domain name.
///
/// A domain name consists of one or more labels. Each label starts with a
Expand All @@ -59,7 +37,7 @@ pub struct Domain {
}

impl TryFrom<String> for Domain {
type Error = DomainError;
type Error = TryFromError;

/// Tries to convert a [`String`] into a `Domain`.
///
Expand All @@ -71,7 +49,7 @@ impl TryFrom<String> for Domain {
///
/// # Example
/// ```
/// use dns_lib::domain::Domain;
/// use dns_lib::Domain;
///
/// let valid_domain = "example.com".to_string();
/// assert!(Domain::try_from(valid_domain).is_ok());
Expand All @@ -87,7 +65,7 @@ impl TryFrom<String> for Domain {
}

impl TryFrom<&[u8]> for Domain {
type Error = DomainError;
type Error = TryFromError;

/// Tries to convert a slice `&[u8]` into a `Domain`.
///
Expand All @@ -99,7 +77,7 @@ impl TryFrom<&[u8]> for Domain {
///
/// # Example
/// ```
/// use dns_lib::domain::Domain;
/// use dns_lib::Domain;
///
/// let valid_domain = b"example.com" as &[u8];
/// assert!(Domain::try_from(valid_domain).is_ok());
Expand All @@ -111,12 +89,12 @@ impl TryFrom<&[u8]> for Domain {
/// [RFC 1034, Section 3.5]: https://datatracker.ietf.org/doc/html/rfc1034#section-3.5
fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
if value.is_empty() {
return Err(DomainError::EmptyDomain);
return Err(TryFromError::DomainEmpty);
}

let raw_labels: Vec<&[u8]> = value.split(|&byte| byte == LABEL_SEPARATOR as u8).collect();

let parsed_labels_result: Result<Vec<String>, DomainError> =
let parsed_labels_result: Result<Vec<String>, TryFromError> =
raw_labels.iter().map(|&slice| parse_label(slice)).collect();

match parsed_labels_result {
Expand All @@ -139,25 +117,25 @@ impl fmt::Display for Domain {
/// and hyphens.
///
/// See [RFC 1034, Section 3.5 - Preferred name syntax](https://datatracker.ietf.org/doc/html/rfc1034#section-3.5)
pub fn parse_label(bytes: &[u8]) -> Result<String, DomainError> {
let label = match std::str::from_utf8(bytes) {
pub fn parse_label(bytes: &[u8]) -> Result<String, TryFromError> {
let label = match std::string::String::from_utf8(bytes.to_vec()) {
Ok(str) => str.to_string(),
Err(_) => return Err(DomainError::InvalidLabelFormat),
Err(e) => return Err(TryFromError::LabelInvalidEncoding(e)),
};

if bytes.len() == 0 {
return Err(DomainError::EmptyLabel);
return Err(TryFromError::LabelEmpty);
}

if bytes.len() > MAX_LABEL_LENGTH {
return Err(DomainError::LabelTooLong);
return Err(TryFromError::LabelTooLong(label));
}

let (first_byte, remaining_bytes) = bytes.split_at(1);
if remaining_bytes.len() == 0 {
match first_byte[0].is_ascii_alphabetic() {
true => return Ok(label),
false => return Err(DomainError::InvalidLabelFormat),
false => return Err(TryFromError::LabelInvalidFormat(label)),
};
}

Expand All @@ -169,7 +147,7 @@ pub fn parse_label(bytes: &[u8]) -> Result<String, DomainError> {

match first_byte_letter && middle_bytes_are_ldh_str && last_byte_letter_digit {
true => Ok(label),
false => Err(DomainError::InvalidLabelFormat),
false => Err(TryFromError::LabelInvalidFormat(label)),
}
}

Expand Down Expand Up @@ -216,17 +194,17 @@ mod tests {
}

#[rstest]
#[case(b"420", "label is invalid".to_string())]
#[case(b"4a", "label is invalid".to_string())]
#[case(b"-", "label is invalid".to_string())]
#[case(b"a-", "label is invalid".to_string())]
#[case(b"ab-", "label is invalid".to_string())]
#[case(b"-a", "label is invalid".to_string())]
#[case(b"bar-", "label is invalid".to_string())]
#[case(b"420", "label '420' has invalid format".to_string())]
#[case(b"4a", "label '4a' has invalid format".to_string())]
#[case(b"-", "label '-' has invalid format".to_string())]
#[case(b"a-", "label 'a-' has invalid format".to_string())]
#[case(b"ab-", "label 'ab-' has invalid format".to_string())]
#[case(b"-a", "label '-a' has invalid format".to_string())]
#[case(b"bar-", "label 'bar-' has invalid format".to_string())]
#[case(b"", "label is empty".to_string())]
#[case(
b"a-label-that-exceeds-the-allowed-limit-of-sixty-three-characters",
"label exceeds the maximum allowed length of 63 characters".to_string()
"label 'a-label-that-exceeds-the-allowed-limit-of-sixty-three-characters' exceeds the maximum allowed length of 63 characters".to_string()
)]
fn parse_label_fails(#[case] input: &[u8], #[case] error_msg: String) {
let result = parse_label(input);
Expand Down Expand Up @@ -263,12 +241,12 @@ mod tests {
}

#[rstest]
#[case("-.com", "label is invalid".to_string())]
#[case("sübway.com", "label is invalid".to_string())]
#[case("-.com", "label '-' has invalid format".to_string())]
#[case("sübway.com", "label 'sübway' has invalid format".to_string())]
#[case("", "domain is empty".to_string())]
#[case(
"a-label-that-exceeds-the-allowed-limit-of-sixty-three-characters.yahoo.com",
"label exceeds the maximum allowed length of 63 characters".to_string()
"label 'a-label-that-exceeds-the-allowed-limit-of-sixty-three-characters' exceeds the maximum allowed length of 63 characters".to_string()
)]
#[case("cdn..com", "label is empty".to_string())]
fn domain_try_from_string_fails(#[case] input: String, #[case] error_msg: String) {
Expand Down
2 changes: 2 additions & 0 deletions lib/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,7 @@

pub mod domain;

pub use domain::{Domain, TryFromError};

/// Version of the library.
pub const VERSION: &str = env!("CARGO_PKG_VERSION");

0 comments on commit 9dddc06

Please sign in to comment.