diff --git a/.idea/acrate.iml b/.idea/acrate.iml index 48f40e0..b5a43c3 100644 --- a/.idea/acrate.iml +++ b/.idea/acrate.iml @@ -3,13 +3,13 @@ - - + + diff --git a/Cargo.toml b/Cargo.toml index 1e23fe2..4a488a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,3 @@ [workspace] resolver = "2" -members = ["acrate_database", "acrate-hostmeta", "acrate-nodeinfo", "acrate-webfinger"] +members = ["acrate_database", "acrate_rd", "acrate-nodeinfo", "acrate-webfinger"] diff --git a/acrate-hostmeta/src/utils.rs b/acrate-hostmeta/src/utils.rs deleted file mode 100644 index fd3022c..0000000 --- a/acrate-hostmeta/src/utils.rs +++ /dev/null @@ -1,8 +0,0 @@ -/// Extract the MIME type from the value of the `Content-Type` header. -pub fn extract_mime_from_content_type(value: &reqwest::header::HeaderValue) -> Option { - let value = value.to_str().ok()?; - match value.split_once("; ") { - None => Some(value.to_string()), - Some((mime, _)) => Some(mime.to_string()), - } -} diff --git a/acrate-hostmeta/Cargo.toml b/acrate_rd/Cargo.toml similarity index 93% rename from acrate-hostmeta/Cargo.toml rename to acrate_rd/Cargo.toml index a95bd02..267c35e 100644 --- a/acrate-hostmeta/Cargo.toml +++ b/acrate_rd/Cargo.toml @@ -1,10 +1,11 @@ [package] -name = "acrate-hostmeta" +name = "acrate_rd" version = "0.2.0" edition = "2021" [dependencies] log = "0.4.22" +mime = "0.3.17" quick-xml = { version = "0.37.0", features = ["overlapped-lists", "serialize"] } reqwest = { version = "0.12.9", features = ["json", "stream"] } serde = { version = "1.0.214", features = ["derive"] } diff --git a/acrate-hostmeta/src/any.rs b/acrate_rd/src/any.rs similarity index 100% rename from acrate-hostmeta/src/any.rs rename to acrate_rd/src/any.rs diff --git a/acrate-hostmeta/src/jrd.rs b/acrate_rd/src/jrd.rs similarity index 79% rename from acrate-hostmeta/src/jrd.rs rename to acrate_rd/src/jrd.rs index 24c7f68..79dc599 100644 --- a/acrate-hostmeta/src/jrd.rs +++ b/acrate_rd/src/jrd.rs @@ -1,14 +1,18 @@ +//! Definition and implementation of [`ResourceDescriptorJRD`]. + use std::collections::HashMap; +use std::str::FromStr; use serde::{Serialize, Deserialize}; use thiserror::Error; use crate::xrd::{ResourceDescriptorLinkXRD, ResourceDescriptorPropertyXRD, ResourceDescriptorTitleXRD, ResourceDescriptorXRD}; -/// A resource descriptor object in JRD format. +/// A resource descriptor in JRD format. /// /// # Specification /// /// - /// - +/// #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ResourceDescriptorJRD { /// The resource this document refers to. @@ -72,7 +76,8 @@ pub struct ResourceDescriptorLinkJRD { /// - /// #[serde(skip_serializing_if = "Option::is_none")] - pub r#type: Option, + #[serde(with = "crate::utils::serde_mime_opt")] + pub r#type: Option, /// URI to the resource put in relation. /// @@ -123,7 +128,7 @@ impl ResourceDescriptorJRD { /// /// ``` /// # tokio_test::block_on(async { - /// use acrate_hostmeta::jrd::ResourceDescriptorJRD; + /// use acrate_rd::jrd::ResourceDescriptorJRD; /// /// let client = reqwest::Client::new(); /// let url: reqwest::Url = "https://junimo.party/.well-known/nodeinfo".parse() @@ -168,14 +173,25 @@ impl ResourceDescriptorJRD { .get(reqwest::header::CONTENT_TYPE) .ok_or(ContentTypeMissing)?; - log::trace!("Extracting MIME type from the `Content-Type` header..."); - let mime_type = crate::utils::extract_mime_from_content_type(content_type) - .ok_or(ContentTypeInvalid)?; + log::trace!("Extracting media type from the `Content-Type` header..."); + let mime_type = content_type.to_str() + .map_err(ContentTypeUnprintable)?; - log::trace!("Ensuring MIME type is acceptable for JRD parsing..."); - if !(mime_type == "application/json" || mime_type == "application/jrd+json") { + log::trace!("Parsing media type: {mime_type:?}"); + let mime_type = mime::Mime::from_str(mime_type) + .map_err(ContentTypeInvalid)?; + + log::trace!("Checking if media type is supported..."); + let mime_is_json = mime_type == mime::APPLICATION_JSON; + log::trace!("Is media type application/json? {mime_is_json:?}"); + let mime_is_jrd = + mime_type.type_() == mime::APPLICATION + && mime_type.subtype() == "jrd" + && mime_type.suffix() == Some(mime::JSON); + log::trace!("Is media type application/jrd+json? {mime_is_jrd:?}"); + if !(mime_is_json || mime_is_jrd) { log::error!("MIME type `{mime_type}` is not acceptable for JRD parsing."); - return Err(ContentTypeInvalid); + return Err(ContentTypeUnsupported); } log::trace!("Attempting to parse response as JSON..."); @@ -245,9 +261,17 @@ pub enum GetJRDError { #[error("the Content-Type header of the response is missing")] ContentTypeMissing, - /// The `Content-Type` header of the response is invalid. - #[error("the Content-Type header of the response is invalid")] - ContentTypeInvalid, + /// The `Content-Type` header of the response can't be converted to a [`str`]. + #[error("the Content-Type header of the response cannot be converted to a &str")] + ContentTypeUnprintable(reqwest::header::ToStrError), + + /// The `Content-Type` header of the response is not a valid [`mime::Mime`] type. + #[error("the Content-Type header of the response is not a valid media type")] + ContentTypeInvalid(mime::FromStrError), + + /// The `Content-Type` header of the response is not a supported [`mime::Mime`] type. + #[error("the Content-Type header of the response is not a supported media type")] + ContentTypeUnsupported, /// The document failed to be parsed as JSON by [`reqwest`]. #[error("the document failed to be parsed as JSON")] diff --git a/acrate-hostmeta/src/lib.rs b/acrate_rd/src/lib.rs similarity index 73% rename from acrate-hostmeta/src/lib.rs rename to acrate_rd/src/lib.rs index dd9bedf..932601d 100644 --- a/acrate-hostmeta/src/lib.rs +++ b/acrate_rd/src/lib.rs @@ -1,4 +1,4 @@ -//! Resource descriptior handler. +//! Rust typing and utilities for the resource descriptior format. //! //! # Specification //! @@ -8,5 +8,4 @@ pub mod jrd; pub mod xrd; pub mod any; - mod utils; diff --git a/acrate_rd/src/utils.rs b/acrate_rd/src/utils.rs new file mode 100644 index 0000000..fecf97b --- /dev/null +++ b/acrate_rd/src/utils.rs @@ -0,0 +1,105 @@ +//! Various utilities reused in the whole crate. + +/// Module to use in `serde(with = ...)` to [`serde`] a [`mime::Mime`]. +#[allow(dead_code)] +pub mod serde_mime { + use std::fmt::Formatter; + use std::str::FromStr; + use serde::de::{Error, Visitor}; + use serde::{Deserializer, Serializer}; + + pub struct MimeVisitor; + + impl<'de> Visitor<'de> for MimeVisitor { + type Value = mime::Mime; + + fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result { + formatter.write_str("a media type (MIME type)") + } + + fn visit_str(self, v: &str) -> Result + where + E: Error, + { + mime::Mime::from_str(v) + .map_err(|_| E::custom("failed to parse media type")) + } + } + + pub fn deserialize<'de, De>(deserializer: De) -> Result<>::Value, De::Error> + where + De: Deserializer<'de> + { + let s = deserializer.deserialize_str(MimeVisitor)?; + Ok(s) + } + + pub fn serialize(data: mime::Mime, serializer: Ser) -> Result + where + Ser: Serializer + { + let s = data.essence_str(); + serializer.serialize_str(s) + } +} + +/// Module to use in `serde(with = ...)` to [`serde`] an [`Option`] of [`mime::Mime`]. +#[allow(dead_code)] +pub mod serde_mime_opt { + use std::fmt::Formatter; + use std::str::FromStr; + use serde::de::{Error, Visitor}; + use serde::{Deserializer, Serializer}; + + pub struct MimeVisitor; + + impl<'de> Visitor<'de> for MimeVisitor { + type Value = Option; + + fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result { + formatter.write_str("optionally, a media type (MIME type)") + } + + fn visit_str(self, v: &str) -> Result + where + E: Error, + { + Ok( + Some( + mime::Mime::from_str(v) + .map_err(|_| E::custom("failed to parse media type"))? + ) + ) + } + + fn visit_none(self) -> Result + where + E: Error, + { + Ok(None) + } + } + + pub fn deserialize<'de, De>(deserializer: De) -> Result<>::Value, De::Error> + where + De: Deserializer<'de> + { + let s = deserializer.deserialize_str(MimeVisitor)?; + Ok(s) + } + + pub fn serialize(data: &Option, serializer: Ser) -> Result + where + Ser: Serializer + { + match data { + None => { + serializer.serialize_none() + } + Some(data) => { + let s = data.essence_str(); + serializer.serialize_str(s) + } + } + } +} diff --git a/acrate-hostmeta/src/xrd.rs b/acrate_rd/src/xrd.rs similarity index 83% rename from acrate-hostmeta/src/xrd.rs rename to acrate_rd/src/xrd.rs index 96e3a73..01a4db3 100644 --- a/acrate-hostmeta/src/xrd.rs +++ b/acrate_rd/src/xrd.rs @@ -1,3 +1,6 @@ +//! Definition and implementation of [`ResourceDescriptorXRD`]. + +use std::str::FromStr; use serde::{Serialize, Deserialize}; use thiserror::Error; use crate::jrd::{ResourceDescriptorJRD, ResourceDescriptorLinkJRD}; @@ -79,7 +82,8 @@ pub struct ResourceDescriptorLinkXRD { /// #[serde(rename = "@type")] #[serde(skip_serializing_if = "Option::is_none")] - pub r#type: Option, + #[serde(with = "crate::utils::serde_mime_opt")] + pub r#type: Option, /// URI to the resource put in relation. /// @@ -163,7 +167,7 @@ impl ResourceDescriptorXRD { /// /// ``` /// # tokio_test::block_on(async { - /// use acrate_hostmeta::xrd::ResourceDescriptorXRD; + /// use acrate_rd::xrd::ResourceDescriptorXRD; /// /// let client = reqwest::Client::new(); /// let url: reqwest::Url = "https://junimo.party/.well-known/host-meta".parse() @@ -205,14 +209,23 @@ impl ResourceDescriptorXRD { .get(reqwest::header::CONTENT_TYPE) .ok_or(ContentTypeMissing)?; - log::trace!("Extracting MIME type from the `Content-Type` header..."); - let mime_type = crate::utils::extract_mime_from_content_type(content_type) - .ok_or(ContentTypeInvalid)?; + log::trace!("Extracting media type from the `Content-Type` header..."); + let mime_type = content_type.to_str() + .map_err(ContentTypeUnprintable)?; - log::trace!("Ensuring MIME type is acceptable for XRD parsing..."); - if mime_type != "application/xrd+xml" { - log::error!("MIME type `{mime_type}` is not acceptable for XRD parsing."); - return Err(ContentTypeInvalid) + log::trace!("Parsing media type: {mime_type:?}"); + let mime_type = mime::Mime::from_str(mime_type) + .map_err(ContentTypeInvalid)?; + + log::trace!("Checking if media type is supported..."); + let mime_is_xrd = + mime_type.type_() == mime::APPLICATION + && mime_type.subtype() == "xrd" + && mime_type.suffix() == Some(mime::XML); + log::trace!("Is media type application/xrd+xml? {mime_is_xrd:?}"); + if !mime_is_xrd { + log::error!("MIME type `{mime_type}` is not acceptable for JRD parsing."); + return Err(ContentTypeUnsupported); } log::trace!("Attempting to parse response as text..."); @@ -290,9 +303,17 @@ pub enum GetXRDError { #[error("the Content-Type header of the response is missing")] ContentTypeMissing, - /// The `Content-Type` header of the response is invalid. - #[error("the Content-Type header of the response is invalid")] - ContentTypeInvalid, + /// The `Content-Type` header of the response can't be converted to a [`str`]. + #[error("the Content-Type header of the response cannot be converted to a &str")] + ContentTypeUnprintable(reqwest::header::ToStrError), + + /// The `Content-Type` header of the response is not a valid [`mime::Mime`] type. + #[error("the Content-Type header of the response is not a valid media type")] + ContentTypeInvalid(mime::FromStrError), + + /// The `Content-Type` header of the response is not a supported [`mime::Mime`] type. + #[error("the Content-Type header of the response is not a supported media type")] + ContentTypeUnsupported, /// The document failed to be decoded as text. #[error("the document failed to be decoded as text")] diff --git a/acrate-hostmeta/tests/hostmeta_tests.rs b/acrate_rd/tests/integration_tests.rs similarity index 87% rename from acrate-hostmeta/tests/hostmeta_tests.rs rename to acrate_rd/tests/integration_tests.rs index 82ece6a..e2b051e 100644 --- a/acrate-hostmeta/tests/hostmeta_tests.rs +++ b/acrate_rd/tests/integration_tests.rs @@ -38,7 +38,7 @@ macro_rules! test_discover_hostmeta { let base: reqwest::Url = $url.parse() .expect("a valid URL"); - let doc = acrate_hostmeta::any::ResourceDescriptor::discover_hostmeta(&client, base) + let doc = acrate_rd::any::ResourceDescriptor::discover_hostmeta(&client, base) .await .expect("host-meta discovery to succeed"); @@ -61,7 +61,7 @@ macro_rules! test_de_ser_jrd { log::info!("Starting document: {:#?}", JRD_DOCUMENT); - let de: acrate_hostmeta::jrd::ResourceDescriptorJRD = serde_json::from_str(JRD_DOCUMENT) + let de: acrate_rd::jrd::ResourceDescriptorJRD = serde_json::from_str(JRD_DOCUMENT) .expect("document to be deserialized successfully"); log::info!("Serialized document: {de:#?}"); @@ -85,11 +85,10 @@ macro_rules! test_de_ser_xrd { $(#[$tag])* fn $id() { init_log(); - let client = make_client(); log::info!("Starting document: {:#?}", XRD_DOCUMENT); - let de: acrate_hostmeta::xrd::ResourceDescriptorXRD = quick_xml::de::from_str(XRD_DOCUMENT) + let de: acrate_rd::xrd::ResourceDescriptorXRD = quick_xml::de::from_str(XRD_DOCUMENT) .expect("document to be deserialized successfully"); log::info!("Serialized document: {de:#?}"); @@ -113,4 +112,5 @@ test_discover_hostmeta!(test_discover_hostmeta_threads_net, "https://threads.net test_discover_hostmeta!(test_discover_hostmeta_ngoa_giao_loan, "https://ngoa.giao.loan", ignore = "does not support host-meta"); test_discover_hostmeta!(test_discover_hostmeta_hollo_social, "https://hollo.social", ignore = "does not support host-meta"); -test_de_ser_jrd!(test_de_ser_sample_junimo_party, "samples/junimo_party.nodeinfo.jrd.json"); +test_de_ser_jrd!(test_de_ser_jrd_sample_junimo_party, "samples/junimo_party.nodeinfo.jrd.json"); +test_de_ser_xrd!(test_de_ser_xrd_sample_junimo_party, "samples/junimo_party.host-meta.xrd.xml"); diff --git a/acrate-hostmeta/tests/samples/junimo_party.host-meta.xrd.xml b/acrate_rd/tests/samples/junimo_party.host-meta.xrd.xml similarity index 100% rename from acrate-hostmeta/tests/samples/junimo_party.host-meta.xrd.xml rename to acrate_rd/tests/samples/junimo_party.host-meta.xrd.xml diff --git a/acrate-hostmeta/tests/samples/junimo_party.nodeinfo.jrd.json b/acrate_rd/tests/samples/junimo_party.nodeinfo.jrd.json similarity index 100% rename from acrate-hostmeta/tests/samples/junimo_party.nodeinfo.jrd.json rename to acrate_rd/tests/samples/junimo_party.nodeinfo.jrd.json