jid: Fix JID parts serde deserialization
serde was bypassing the validation these types rely on, making them completely unsound in an XMPP context. Fixes #165.
This commit is contained in:
parent
f1e5ac6ac8
commit
60bf821c63
2 changed files with 135 additions and 0 deletions
|
|
@ -2,6 +2,9 @@ Version NEXT:
|
||||||
|
|
||||||
Version 0.12.1, release 2025-11-02:
|
Version 0.12.1, release 2025-11-02:
|
||||||
* Changes:
|
* Changes:
|
||||||
|
- Make serde deserialization actually validate the `NodePart`, `DomainPart`
|
||||||
|
and `ResourcePart`, it was previously completely unsound, creating these
|
||||||
|
types without validating their invariants.
|
||||||
- The 'quote' feature now uses `jid::Jid` instead of `::jid::Jid` to
|
- The 'quote' feature now uses `jid::Jid` instead of `::jid::Jid` to
|
||||||
stop requiring importing the module as a dependency of the project. The
|
stop requiring importing the module as a dependency of the project. The
|
||||||
`jid` module just needs to be made available, for example: `use
|
`jid` module just needs to be made available, for example: `use
|
||||||
|
|
|
||||||
132
jid/src/parts.rs
132
jid/src/parts.rs
|
|
@ -44,6 +44,39 @@ macro_rules! def_part_into_inner_doc {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct NodeDeserializer<'a>(&'a str);
|
||||||
|
|
||||||
|
impl TryFrom<NodeDeserializer<'_>> for NodePart {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(deserializer: NodeDeserializer) -> Result<NodePart, Self::Error> {
|
||||||
|
Ok(NodePart::new(deserializer.0)?.into_owned())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct DomainDeserializer<'a>(&'a str);
|
||||||
|
|
||||||
|
impl TryFrom<DomainDeserializer<'_>> for DomainPart {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(deserializer: DomainDeserializer) -> Result<DomainPart, Self::Error> {
|
||||||
|
Ok(DomainPart::new(deserializer.0)?.into_owned())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct ResourceDeserializer<'a>(&'a str);
|
||||||
|
|
||||||
|
impl TryFrom<ResourceDeserializer<'_>> for ResourcePart {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(deserializer: ResourceDeserializer) -> Result<ResourcePart, Self::Error> {
|
||||||
|
Ok(ResourcePart::new(deserializer.0)?.into_owned())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
macro_rules! def_part_types {
|
macro_rules! def_part_types {
|
||||||
(
|
(
|
||||||
$(#[$mainmeta:meta])*
|
$(#[$mainmeta:meta])*
|
||||||
|
|
@ -203,6 +236,7 @@ def_part_types! {
|
||||||
///
|
///
|
||||||
/// The corresponding slice type is [`NodeRef`].
|
/// The corresponding slice type is [`NodeRef`].
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
|
#[cfg_attr(feature = "serde", serde(try_from = "NodeDeserializer"))]
|
||||||
pub struct NodePart(String) use node_check();
|
pub struct NodePart(String) use node_check();
|
||||||
|
|
||||||
/// `str`-like type which conforms to the requirements of [`NodePart`].
|
/// `str`-like type which conforms to the requirements of [`NodePart`].
|
||||||
|
|
@ -217,6 +251,7 @@ def_part_types! {
|
||||||
/// (optional) `/` in any [`Jid`][crate::Jid], whether
|
/// (optional) `/` in any [`Jid`][crate::Jid], whether
|
||||||
/// [`BareJid`][crate::BareJid] or [`FullJid`][crate::FullJid].
|
/// [`BareJid`][crate::BareJid] or [`FullJid`][crate::FullJid].
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
|
#[cfg_attr(feature = "serde", serde(try_from = "DomainDeserializer"))]
|
||||||
pub struct DomainPart(String) use domain_check();
|
pub struct DomainPart(String) use domain_check();
|
||||||
|
|
||||||
/// `str`-like type which conforms to the requirements of [`DomainPart`].
|
/// `str`-like type which conforms to the requirements of [`DomainPart`].
|
||||||
|
|
@ -230,6 +265,7 @@ def_part_types! {
|
||||||
/// The [`ResourcePart`] is the optional part after the `/` in a
|
/// The [`ResourcePart`] is the optional part after the `/` in a
|
||||||
/// [`Jid`][crate::Jid]. It is mandatory in [`FullJid`][crate::FullJid].
|
/// [`Jid`][crate::Jid]. It is mandatory in [`FullJid`][crate::FullJid].
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
|
#[cfg_attr(feature = "serde", serde(try_from = "ResourceDeserializer"))]
|
||||||
pub struct ResourcePart(String) use resource_check();
|
pub struct ResourcePart(String) use resource_check();
|
||||||
|
|
||||||
/// `str`-like type which conforms to the requirements of
|
/// `str`-like type which conforms to the requirements of
|
||||||
|
|
@ -292,4 +328,100 @@ mod tests {
|
||||||
assert_eq!(n1, n3);
|
assert_eq!(n1, n3);
|
||||||
assert_ne!(n1, n2);
|
assert_ne!(n1, n2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "serde")]
|
||||||
|
#[test]
|
||||||
|
fn nodepart_serde() {
|
||||||
|
serde_test::assert_de_tokens(
|
||||||
|
&NodePart(String::from("test")),
|
||||||
|
&[
|
||||||
|
serde_test::Token::TupleStruct {
|
||||||
|
name: "NodePart",
|
||||||
|
len: 1,
|
||||||
|
},
|
||||||
|
serde_test::Token::BorrowedStr("test"),
|
||||||
|
serde_test::Token::TupleStructEnd,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
serde_test::assert_de_tokens_error::<NodePart>(
|
||||||
|
&[
|
||||||
|
serde_test::Token::TupleStruct {
|
||||||
|
name: "NodePart",
|
||||||
|
len: 1,
|
||||||
|
},
|
||||||
|
serde_test::Token::BorrowedStr("invalid@domain"),
|
||||||
|
serde_test::Token::TupleStructEnd,
|
||||||
|
],
|
||||||
|
"localpart doesn’t pass nodeprep validation",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "serde")]
|
||||||
|
#[test]
|
||||||
|
fn domainpart_serde() {
|
||||||
|
serde_test::assert_de_tokens(
|
||||||
|
&DomainPart(String::from("[::1]")),
|
||||||
|
&[
|
||||||
|
serde_test::Token::TupleStruct {
|
||||||
|
name: "DomainPart",
|
||||||
|
len: 1,
|
||||||
|
},
|
||||||
|
serde_test::Token::BorrowedStr("[::1]"),
|
||||||
|
serde_test::Token::TupleStructEnd,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
serde_test::assert_de_tokens(
|
||||||
|
&DomainPart(String::from("domain.example")),
|
||||||
|
&[
|
||||||
|
serde_test::Token::TupleStruct {
|
||||||
|
name: "DomainPart",
|
||||||
|
len: 1,
|
||||||
|
},
|
||||||
|
serde_test::Token::BorrowedStr("domain.example"),
|
||||||
|
serde_test::Token::TupleStructEnd,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
serde_test::assert_de_tokens_error::<DomainPart>(
|
||||||
|
&[
|
||||||
|
serde_test::Token::TupleStruct {
|
||||||
|
name: "DomainPart",
|
||||||
|
len: 1,
|
||||||
|
},
|
||||||
|
serde_test::Token::BorrowedStr("invalid@domain"),
|
||||||
|
serde_test::Token::TupleStructEnd,
|
||||||
|
],
|
||||||
|
"domain doesn’t pass idna validation",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "serde")]
|
||||||
|
#[test]
|
||||||
|
fn resourcepart_serde() {
|
||||||
|
serde_test::assert_de_tokens(
|
||||||
|
&ResourcePart(String::from("test")),
|
||||||
|
&[
|
||||||
|
serde_test::Token::TupleStruct {
|
||||||
|
name: "ResourcePart",
|
||||||
|
len: 1,
|
||||||
|
},
|
||||||
|
serde_test::Token::BorrowedStr("test"),
|
||||||
|
serde_test::Token::TupleStructEnd,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
serde_test::assert_de_tokens_error::<ResourcePart>(
|
||||||
|
&[
|
||||||
|
serde_test::Token::TupleStruct {
|
||||||
|
name: "ResourcePart",
|
||||||
|
len: 1,
|
||||||
|
},
|
||||||
|
serde_test::Token::BorrowedStr("🤖"),
|
||||||
|
serde_test::Token::TupleStructEnd,
|
||||||
|
],
|
||||||
|
"resource doesn’t pass resourceprep validation",
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue