w5500-hl 0.9.0

Driver for the Wiznet W5500 internet offload chip.
Documentation
/// A validated hostname.
///
/// This is not used within this crate, it is provided here for crates
/// implementing protocols such as DNS and DHCP to use.
#[derive(Debug, Clone, Copy)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct Hostname<'a> {
    hostname: &'a str,
}

#[allow(clippy::len_without_is_empty)] // empty is not allowed by `new`
impl<'a> Hostname<'a> {
    /// Create a new hostname.
    ///
    /// This validates the hostname for [RFC-1035] compliance:
    ///
    /// A hostname is valid if the following condition are true:
    ///
    /// - It does not start or end with `'-'` or `'.'`.
    /// - It does not contain any characters outside of the alphanumeric range, except for `'-'` and `'.'`.
    /// - It is not empty.
    /// - It is 253 or fewer characters.
    /// - Its labels (characters separated by `.`) are not empty.
    /// - Its labels are 63 or fewer characters.
    /// - Its labels do not start or end with `'-'` or `'.'`.
    ///
    /// # Example
    ///
    /// ```
    /// use w5500_hl::Hostname;
    ///
    /// assert!(Hostname::new("is-valid-example").is_some());
    /// assert!(Hostname::new("this-is-not-?-valid").is_none());
    /// ```
    ///
    /// [RFC-1035]: https://www.rfc-editor.org/rfc/rfc1035
    pub const fn new(hostname: &'a str) -> Option<Self> {
        // This function is very ugly because of const limitations on stable
        // for the refined non-const version see TryFrom<&str> below.

        const fn is_valid_char(byte: u8) -> bool {
            (byte >= b'a' && byte <= b'z')
                || (byte >= b'A' && byte <= b'Z')
                || (byte >= b'0' && byte <= b'9')
                || byte == b'-'
                || byte == b'.'
        }

        if hostname.is_empty() || hostname.len() > 253 {
            return None;
        }

        const fn is_valid_segment(hostname: &str, start: usize, end: usize) -> bool {
            let segment_length: usize = end - start;
            if segment_length == 0 || segment_length > 63 {
                return false;
            }

            let first_byte_label: u8 = hostname.as_bytes()[start];
            if first_byte_label == b'-' {
                return false;
            }

            let last_byte_label: u8 = hostname.as_bytes()[end - 1];
            if last_byte_label == b'-' {
                return false;
            }

            true
        }

        let mut idx: usize = 0;
        let mut segment_start: usize = 0;
        while idx < hostname.len() {
            let byte: u8 = hostname.as_bytes()[idx];
            if !is_valid_char(byte) {
                return None;
            }
            if byte == b'.' {
                if !is_valid_segment(hostname, segment_start, idx) {
                    return None;
                }

                segment_start = idx + 1;
            }
            idx += 1;
        }

        if !is_valid_segment(hostname, segment_start, idx) {
            return None;
        }

        Some(Self { hostname })
    }

    /// Create a new hostname, panicking if the hostname is invalid.
    ///
    /// # Panics
    ///
    /// This is the same as [`new`](Self::new), but it will panic on invalid
    /// hostnames.
    ///
    /// This should only be used in `const` contexts where the evaluation will
    /// fail at compile time.
    ///
    /// # Example
    ///
    /// ```
    /// use w5500_hl::Hostname;
    ///
    /// const MY_HOSTNAME: Hostname = Hostname::new_unwrapped("valid.hostname");
    /// ```
    pub const fn new_unwrapped(hostname: &'a str) -> Self {
        match Self::new(hostname) {
            Some(hostname) => hostname,
            None => ::core::panic!("invalid hostname"),
        }
    }

    /// Returns an iterator over the labels of the hostname.
    ///
    /// # Example
    ///
    /// ```
    /// use core::str::Split;
    /// use w5500_hl::Hostname;
    ///
    /// const DOCS_RS: Hostname = Hostname::new_unwrapped("docs.rs");
    /// let mut lables: Split<char> = DOCS_RS.labels();
    ///
    /// assert_eq!(lables.next(), Some("docs"));
    /// assert_eq!(lables.next(), Some("rs"));
    /// assert_eq!(lables.next(), None);
    /// ```
    #[inline]
    pub fn labels(&self) -> core::str::Split<'a, char> {
        self.hostname.split('.')
    }

    /// Length of the hostname in bytes.
    ///
    /// # Example
    ///
    /// ```
    /// use w5500_hl::Hostname;
    ///
    /// const DOCS_RS: Hostname = Hostname::new_unwrapped("docs.rs");
    ///
    /// assert_eq!(DOCS_RS.len(), 7);
    /// ```
    #[inline]
    pub fn len(&self) -> u8 {
        // truncation is OK, hostname is validated to be 255 bytes or fewer
        self.hostname.len() as u8
    }

    /// Create a new hostname without checking for validity.
    ///
    /// # Safety
    ///
    /// The `hostname` argument must meet all the conditions for validity
    /// described in [`new`](Self::new).
    ///
    /// # Example
    ///
    /// ```
    /// use w5500_hl::Hostname;
    ///
    /// // safety: doc.rs is a valid hostname
    /// const DOCS_RS: Hostname = unsafe { Hostname::new_unchecked("docs.rs") };
    /// ```
    #[allow(unsafe_code)]
    #[inline]
    pub const unsafe fn new_unchecked(hostname: &'a str) -> Self {
        Self { hostname }
    }

    /// Converts the hostname to a byte slice.
    ///
    /// # Example
    ///
    /// ```
    /// use w5500_hl::Hostname;
    ///
    /// const DOCS_RS: Hostname = Hostname::new_unwrapped("docs.rs");
    /// assert_eq!(DOCS_RS.as_bytes(), [100, 111, 99, 115, 46, 114, 115]);
    /// ```
    #[inline]
    pub const fn as_bytes(&self) -> &[u8] {
        self.hostname.as_bytes()
    }
}

/// The error type returned when a str to [`Hostname`] conversion fails.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct TryFromStrError(pub(crate) ());

impl<'a> TryFrom<&'a str> for Hostname<'a> {
    type Error = TryFromStrError;

    fn try_from(hostname: &'a str) -> Result<Self, Self::Error> {
        fn is_valid_char(byte: u8) -> bool {
            (b'a'..=b'z').contains(&byte)
                || (b'A'..=b'Z').contains(&byte)
                || (b'0'..=b'9').contains(&byte)
                || byte == b'-'
                || byte == b'.'
        }

        if hostname.is_empty()
            || hostname.len() > 253
            || hostname.bytes().any(|byte| !is_valid_char(byte))
            || hostname.split('.').any(|label| {
                label.is_empty()
                    || label.len() > 63
                    || label.ends_with('-')
                    || label.starts_with('-')
            })
        {
            Err(TryFromStrError(()))
        } else {
            Ok(Self { hostname })
        }
    }
}

impl<'a> From<Hostname<'a>> for &'a str {
    #[inline]
    fn from(hostname: Hostname<'a>) -> Self {
        hostname.hostname
    }
}

#[cfg(test)]
mod tests {
    use super::Hostname;

    #[test]
    fn valid_hostnames() {
        for hostname in &[
            "VaLiD-HoStNaMe",
            "50-name",
            "235235",
            "example.com",
            "VaLid.HoStNaMe",
            "123.456",
            "one-byte.a.label",
        ] {
            assert!(Hostname::new(hostname).is_some(), "{hostname} is not valid");
            assert!(
                Hostname::try_from(*hostname).is_ok(),
                "{hostname} is not valid"
            );
        }
    }

    #[test]
    fn invalid_hostnames() {
        for hostname in &[
            "-invalid-name",
            "also-invalid-",
            "asdf@fasd",
            "@asdfl",
            "asd f@",
            ".invalid",
            "invalid.name.",
            "invalid.-starting.char",
            "invalid.ending-.char",
            "empty..label",
            "..empty-starting-label",
            "empty-ending-label..",
            "label-is-way-to-longgggggggggggggggggggggggggggggggggggggggggggg.com",
        ] {
            assert!(
                Hostname::new(hostname).is_none(),
                "{hostname} should not be valid"
            );
            assert!(
                Hostname::try_from(*hostname).is_err(),
                "{hostname} should not be valid"
            );
        }
    }
}