ascii_domain

Domains whose labels are only ASCII.
git clone https://git.philomathiclife.com/repos/ascii_domain
Log | Files | Refs | README

commit f3b6ea7df4cce7a861a7a77fa574a07b10b64ea5
parent e64e9a57bf7ee0554ed1bfe21d49a1ed0a6124f3
Author: Zack Newman <zack@philomathiclife.com>
Date:   Mon,  5 Feb 2024 22:34:44 -0700

add serde support

Diffstat:
MCargo.toml | 18+++++++++++++++++-
Abuild.rs | 12++++++++++++
Msrc/dom.rs | 13++++++-------
Msrc/lib.rs | 5+++++
Asrc/serde.rs | 223+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 263 insertions(+), 8 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml @@ -9,12 +9,28 @@ license = "MIT OR Apache-2.0" name = "ascii_domain" readme = "README.md" repository = "https://git.philomathiclife.com/repos/ascii_domain/" -version = "0.3.0" +version = "0.3.1" [lib] name = "ascii_domain" path = "src/lib.rs" +[dependencies] +serde = { version = "1.0.196", default-features = false, features = ["alloc"], optional = true } + +[dev-dependencies] +serde_json = { version = "1.0.113", default-features = false, features = ["alloc"] } + +[build-dependencies] +rustc_version = "0.4.0" + +[features] +serde = ["dep:serde"] + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + [badges] maintenance = { status = "actively-developed" } diff --git a/build.rs b/build.rs @@ -0,0 +1,12 @@ +use rustc_version::{version_meta, Channel}; + +fn main() { + // Set cfg flags depending on release channel + let channel = match version_meta().unwrap().channel { + Channel::Stable => "CHANNEL_STABLE", + Channel::Beta => "CHANNEL_BETA", + Channel::Nightly => "CHANNEL_NIGHTLY", + Channel::Dev => "CHANNEL_DEV", + }; + println!("cargo:rustc-cfg={}", channel) +} diff --git a/src/dom.rs b/src/dom.rs @@ -989,9 +989,7 @@ impl Display for Rfc1123Err { } } impl Error for Rfc1123Err {} -/// **TL;DR** -/// -/// Wrapper type around a [`Domain`] that enforces conformance to +/// **TL;DR** Wrapper type around a [`Domain`] that enforces conformance to /// [RFC 1123](https://www.rfc-editor.org/rfc/rfc1123#page-13). /// /// * Each [`Label`] must only contain ASCII digits, letters, or hyphen. @@ -1000,10 +998,10 @@ impl Error for Rfc1123Err {} /// --- /// Unsurprisingly, RFC 1123 is not super precise as it uses "host name" to mean label and also domain: /// "Host software MUST handle host names \[labels\] of up to 63 characters and SHOULD handle host -/// names \[domains\] of up to 255 characters". It also states that only "one aspect of host name syntax is hereby -/// changed" from [RFC 952](https://www.rfc-editor.org/rfc/rfc952): "the restriction on the first character -/// is relaxed to allow either a letter or a digit". Despite that, it goes on to mention other restrictions -/// not mentioned in RFC 952: "the highest-level component label will be alphabetic". It is therefore +/// names \[domains\] of up to 255 characters". It also states that only "one aspect of host name \[label\] +/// syntax is hereby changed" from [RFC 952](https://www.rfc-editor.org/rfc/rfc952): "the restriction on the +/// first character is relaxed to allow either a letter or a digit". Despite that, it goes on to mention other +/// restrictions not mentioned in RFC 952: "the highest-level component label will be alphabetic". It is therefore /// important to understand how this type interprets that RFC and why it does so. /// /// The primary issue with RFC 1123 is the unjustified comment about the TLD being alphabetic. It is given @@ -1091,6 +1089,7 @@ impl<T: AsRef<[u8]>> Rfc1123Domain<T> { /// Note that due to the most relaxed interpretation of RFC 1123 mentioned in [`Rfc1123Domain`], it is possible /// for the domain to be an IPv4 address unlike the strictest, strict, literal, and possibly relaxed /// interpretations. + /// /// # Example /// /// ``` diff --git a/src/lib.rs b/src/lib.rs @@ -13,6 +13,7 @@ //! all octets are allowed; but conforming to [RFC 1123](https://www.rfc-editor.org/rfc/rfc1123) or //! [RFC 5891](https://datatracker.ietf.org/doc/html/rfc5891) requires stricter formats and a reduced character //! set. +#![cfg_attr(all(doc, CHANNEL_NIGHTLY), feature(doc_auto_cfg))] #![deny( unsafe_code, unused, @@ -44,3 +45,7 @@ pub mod char_set; /// [`char_set::AllowedAscii`]. Also contains [`dom::Rfc1123Domain`] which is a `Domain` that conforms to /// [RFC 1123](https://www.rfc-editor.org/rfc/rfc1123#page-13). pub mod dom; +/// Contains a Serde [`Visitor`](https://docs.rs/serde/latest/serde/de/trait.Visitor.html) that can be used to help +/// deserialize [`dom::Domain`] wrappers. +#[cfg(feature = "serde")] +pub mod serde; diff --git a/src/serde.rs b/src/serde.rs @@ -0,0 +1,223 @@ +#[cfg(feature = "serde")] +use crate::{ + char_set::{AllowedAscii, ASCII_HYPHEN_DIGITS_LETTERS, PRINTABLE_ASCII}, + dom::{Domain, Rfc1123Domain}, +}; +#[cfg(feature = "serde")] +use core::fmt; +#[cfg(feature = "serde")] +use core::marker::PhantomData; +#[cfg(feature = "serde")] +use serde::{ + de::{self, Deserialize, Deserializer, Error, Unexpected, Visitor}, + ser::{Serialize, Serializer}, +}; +#[cfg(feature = "serde")] +impl<T: AsRef<[u8]>> Serialize for Domain<T> { + /// Serializes `Domain` as a string. + #[inline] + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + serializer.serialize_str(self.as_str()) + } +} +#[cfg(feature = "serde")] +impl<T: AsRef<[u8]>> Serialize for Rfc1123Domain<T> { + /// Serializes `Rfc1123Domain` as a string. + #[inline] + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + serializer.serialize_str(self.as_str()) + } +} +/// Serde [`Visitor`] that deserializes a string into a [`Domain`] based on [`Self::allowed_ascii`]. +/// +/// Since `Domain`s rely on an [`AllowedAscii`], there cannot be a single deserializer. This visitor +/// makes it slightly easier to implement [`Deserialize`] for `Domain` wrappers based on whatever `AllowedAscii` +/// is desired. +/// +/// # Example +/// +/// ``` +/// use ascii_domain::{dom::Domain, char_set::ASCII_HYPHEN_DIGITS_LETTERS, serde::DomainVisitor}; +/// use serde::de::{Deserialize, Deserializer}; +/// struct DomainWrapper(Domain<String>); +/// impl<'de> Deserialize<'de> for DomainWrapper { +/// fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> +/// where +/// D: Deserializer<'de>, +/// { +/// deserializer.deserialize_string(DomainVisitor::<'_, _, String>::new(&ASCII_HYPHEN_DIGITS_LETTERS)).map(|dom| DomainWrapper(dom)) +/// } +/// } +/// ``` +#[allow(clippy::partial_pub_fields)] +#[cfg(feature = "serde")] +#[derive(Clone, Copy)] +pub struct DomainVisitor<'a, T, T2> { + /// Phantom. + _x: PhantomData<fn() -> T2>, + /// The character set the visitor will use when deserializing a string into a `Domain`. + pub allowed_ascii: &'a AllowedAscii<T>, +} +impl<'a, T, T2> DomainVisitor<'a, T, T2> { + /// Returns `DomainVisitor` with [`Self::allowed_ascii`] set to `allowed_ascii`. + /// + /// # Example + /// + /// ``` + /// use ascii_domain::{char_set::ASCII_HYPHEN_DIGITS_LETTERS, serde::DomainVisitor}; + /// assert!(DomainVisitor::<'_, _, String>::new(&ASCII_HYPHEN_DIGITS_LETTERS).allowed_ascii.len() == 63); + /// ``` + #[inline] + pub const fn new<'b: 'a>(allowed_ascii: &'b AllowedAscii<T>) -> Self { + Self { + _x: PhantomData, + allowed_ascii, + } + } +} +#[cfg(feature = "serde")] +impl<'de: 'b, 'a, 'b, T: AsRef<[u8]>> Visitor<'de> for DomainVisitor<'a, T, &'b str> { + type Value = Domain<&'b str>; + #[inline] + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("Domain") + } + #[inline] + fn visit_borrowed_str<E>(self, v: &'de str) -> Result<Self::Value, E> + where + E: de::Error, + { + Domain::try_from_bytes(v, self.allowed_ascii) + .map_err(|err| E::invalid_value(Unexpected::Str(err.to_string().as_str()), &"a Domain")) + } +} +#[cfg(feature = "serde")] +impl<'de, 'a, T: AsRef<[u8]>> Visitor<'de> for DomainVisitor<'a, T, String> { + type Value = Domain<String>; + #[inline] + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("Domain") + } + #[inline] + fn visit_string<E>(self, v: String) -> Result<Self::Value, E> + where + E: de::Error, + { + Domain::try_from_bytes(v, self.allowed_ascii) + .map_err(|err| E::invalid_value(Unexpected::Str(err.to_string().as_str()), &"a Domain")) + } + #[inline] + fn visit_str<E>(self, v: &str) -> Result<Self::Value, E> + where + E: de::Error, + { + self.visit_string(v.to_owned()) + } +} +/// Deserializes `String`s into a `Domain` based on [`PRINTABLE_ASCII`]. +#[cfg(feature = "serde")] +impl<'de> Deserialize<'de> for Domain<String> { + #[inline] + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + deserializer.deserialize_string(DomainVisitor::<'_, _, String>::new(&PRINTABLE_ASCII)) + } +} +/// Deserializes `str`s into a `Domain` based on [`PRINTABLE_ASCII`]. +#[cfg(feature = "serde")] +impl<'de: 'a, 'a> Deserialize<'de> for Domain<&'a str> { + #[inline] + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + deserializer.deserialize_string(DomainVisitor::<'_, _, &str>::new(&PRINTABLE_ASCII)) + } +} +#[cfg(feature = "serde")] +impl<'de> Deserialize<'de> for Rfc1123Domain<String> { + #[inline] + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + deserializer + .deserialize_string(DomainVisitor::<'_, _, String>::new( + &ASCII_HYPHEN_DIGITS_LETTERS, + )) + .and_then(|dom| { + Self::try_from(dom).map_err(|err| { + Error::invalid_value( + Unexpected::Str(err.to_string().as_str()), + &"an Rfc1123Domain", + ) + }) + }) + } +} +#[cfg(feature = "serde")] +impl<'de: 'a, 'a> Deserialize<'de> for Rfc1123Domain<&'a str> { + #[inline] + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + deserializer + .deserialize_string(DomainVisitor::<'_, _, &str>::new( + &ASCII_HYPHEN_DIGITS_LETTERS, + )) + .and_then(|dom| { + Self::try_from(dom).map_err(|err| { + Error::invalid_value( + Unexpected::Str(err.to_string().as_str()), + &"an Rfc1123Domain", + ) + }) + }) + } +} +#[cfg(all(test, feature = "serde"))] +mod tests { + use crate::{ + char_set::ASCII_HYPHEN_DIGITS_LETTERS, + dom::{Domain, Rfc1123Domain}, + }; + use serde_json; + #[test] + fn test_serde() { + assert!(serde_json::from_str::<Domain<&str>>(r#""example.com""#) + .map_or(false, |dom| dom.label_count().get() == 2)); + assert!(serde_json::from_str::<Domain<String>>(r#""c\"om""#) + .map_or(false, |dom| dom.label_count().get() == 1)); + // Can't borrow since input needs to be de-escaped. + assert!(serde_json::from_str::<Domain<&str>>(r#""c\"om""#) + .map_or_else(|err| err.is_data() && err.column() == 7, |_| false)); + assert!(serde_json::to_string( + &Domain::try_from_bytes("example.com", &ASCII_HYPHEN_DIGITS_LETTERS).unwrap() + ) + .map_or(false, |output| output == r#""example.com""#)); + assert!(serde_json::to_string( + &Domain::try_from_bytes(b"example.com", &ASCII_HYPHEN_DIGITS_LETTERS).unwrap() + ) + .map_or(false, |output| output == r#""example.com""#)); + assert!( + serde_json::from_str::<Rfc1123Domain<&str>>(r#""example.com""#) + .map_or(false, |dom| dom.label_count().get() == 2) + ); + assert!( + serde_json::from_str::<Rfc1123Domain<String>>(r#""c\u006fm""#) + .map_or(false, |dom| dom.tld().as_str() == "com") + ); + // Can't borrow since input needs to be de-escaped. + assert!(serde_json::from_str::<Rfc1123Domain<&str>>(r#""c\u006fm""#) + .map_or_else(|err| err.is_data() && err.column() == 10, |_| false)); + } +}