rxml 0.13.0 changed the quoting style to make it consistent, so we have to patch up all affected places where we match exact strings.
468 lines
16 KiB
Rust
468 lines
16 KiB
Rust
// Copyright (c) 2024 xmpp-rs contributors.
|
|
//
|
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
|
|
use alloc::borrow::Cow;
|
|
use core::fmt;
|
|
use core::str::FromStr;
|
|
|
|
use minidom::IntoAttributeValue;
|
|
use xso::{error::Error, AsXml, AsXmlText, FromXml, FromXmlText};
|
|
|
|
use crate::ns;
|
|
|
|
/// Validation Method
|
|
#[derive(FromXml, AsXml, Debug, Clone, PartialEq)]
|
|
#[xml(namespace = ns::XDATA_VALIDATE)]
|
|
pub enum Method {
|
|
/// … to indicate that the value(s) should simply match the field type and datatype constraints,
|
|
/// the `<validate/>` element shall contain a `<basic/>` child element. Using `<basic/>` validation,
|
|
/// the form interpreter MUST follow the validation rules of the datatype (if understood) and
|
|
/// the field type.
|
|
///
|
|
/// <https://xmpp.org/extensions/xep-0122.html#usercases-validation.basic>
|
|
#[xml(name = "basic")]
|
|
Basic,
|
|
|
|
/// For "list-single" or "list-multi", to indicate that the user may enter a custom value
|
|
/// (matching the datatype constraints) or choose from the predefined values, the `<validate/>`
|
|
/// element shall contain an `<open/>` child element. The `<open/>` validation method applies to
|
|
/// "text-multi" differently; it hints that each value for a "text-multi" field shall be
|
|
/// validated separately. This effectively turns "text-multi" fields into an open-ended
|
|
/// "list-multi", with no options and all values automatically selected.
|
|
///
|
|
/// <https://xmpp.org/extensions/xep-0122.html#usercases-validation.open>
|
|
#[xml(name = "open")]
|
|
Open,
|
|
|
|
/// To indicate that the value should fall within a certain range, the `<validate/>` element shall
|
|
/// contain a `<range/>` child element. The 'min' and 'max' attributes of the `<range/>` element
|
|
/// specify the minimum and maximum values allowed, respectively.
|
|
///
|
|
/// The 'max' attribute specifies the maximum allowable value. This attribute is OPTIONAL.
|
|
/// The value depends on the datatype in use.
|
|
///
|
|
/// The 'min' attribute specifies the minimum allowable value. This attribute is OPTIONAL.
|
|
/// The value depends on the datatype in use.
|
|
///
|
|
/// The `<range/>` element SHOULD possess either a 'min' or 'max' attribute, and MAY possess both.
|
|
/// If neither attribute is included, the processor MUST assume that there are no range
|
|
/// constraints.
|
|
///
|
|
/// <https://xmpp.org/extensions/xep-0122.html#usercases-validation.range>
|
|
#[xml(name = "range")]
|
|
Range {
|
|
/// The 'min' attribute specifies the minimum allowable value.
|
|
#[xml(attribute(default))]
|
|
min: Option<String>,
|
|
|
|
/// The 'max' attribute specifies the maximum allowable value.
|
|
#[xml(attribute(default))]
|
|
max: Option<String>,
|
|
},
|
|
|
|
/// To indicate that the value should be restricted to a regular expression, the `<validate/>`
|
|
/// element shall contain a `<regex/>` child element. The XML character data of this element is
|
|
/// the pattern to apply. The syntax of this content MUST be that defined for POSIX extended
|
|
/// regular expressions, including support for Unicode. The `<regex/>` element MUST contain
|
|
/// character data only.
|
|
///
|
|
/// <https://xmpp.org/extensions/xep-0122.html#usercases-validatoin.regex>
|
|
#[xml(name = "regex")]
|
|
Regex(#[xml(text)] String),
|
|
}
|
|
|
|
/// Selection Ranges in "list-multi"
|
|
#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
|
|
#[xml(namespace = ns::XDATA_VALIDATE, name = "list-range")]
|
|
pub struct ListRange {
|
|
/// The 'min' attribute specifies the minimum allowable number of selected/entered values.
|
|
#[xml(attribute(default))]
|
|
pub min: Option<u32>,
|
|
|
|
/// The 'max' attribute specifies the maximum allowable number of selected/entered values.
|
|
#[xml(attribute(default))]
|
|
pub max: Option<u32>,
|
|
}
|
|
|
|
/// Enum representing errors that can occur while parsing a `Datatype`.
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
pub enum DatatypeError {
|
|
/// Error indicating that a prefix is missing in the validation datatype.
|
|
MissingPrefix {
|
|
/// The invalid string that caused this error.
|
|
input: String,
|
|
},
|
|
|
|
/// Error indicating that the validation datatype is invalid.
|
|
InvalidType {
|
|
/// The invalid string that caused this error.
|
|
input: String,
|
|
},
|
|
|
|
/// Error indicating that the validation datatype is unknown.
|
|
UnknownType {
|
|
/// The invalid string that caused this error.
|
|
input: String,
|
|
},
|
|
}
|
|
|
|
impl core::error::Error for DatatypeError {}
|
|
|
|
impl fmt::Display for DatatypeError {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
DatatypeError::MissingPrefix { input } => {
|
|
write!(f, "Missing prefix in validation datatype {input:?}.")
|
|
}
|
|
DatatypeError::InvalidType { input } => {
|
|
write!(f, "Invalid validation datatype {input:?}.")
|
|
}
|
|
DatatypeError::UnknownType { input } => {
|
|
write!(f, "Unknown validation datatype {input:?}.")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Data Forms Validation Datatypes
|
|
///
|
|
/// <https://xmpp.org/registrar/xdv-datatypes.html>
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
pub enum Datatype {
|
|
/// A Uniform Resource Identifier Reference (URI)
|
|
AnyUri,
|
|
|
|
/// An integer with the specified min/max
|
|
/// Min: -128, Max: 127
|
|
Byte,
|
|
|
|
/// A calendar date
|
|
Date,
|
|
|
|
/// A specific instant of time
|
|
DateTime,
|
|
|
|
/// An arbitrary-precision decimal number
|
|
Decimal,
|
|
|
|
/// An IEEE double-precision 64-bit floating point type
|
|
Double,
|
|
|
|
/// An integer with the specified min/max
|
|
/// Min: -2147483648, Max: 2147483647
|
|
Int,
|
|
|
|
/// A decimal number with no fraction digits
|
|
Integer,
|
|
|
|
/// A language identifier as defined by RFC 1766
|
|
Language,
|
|
|
|
/// An integer with the specified min/max
|
|
/// Min: -9223372036854775808, Max: 9223372036854775807
|
|
Long,
|
|
|
|
/// An integer with the specified min/max
|
|
/// Min: -32768, Max: 32767
|
|
Short,
|
|
|
|
/// A character strings in XML
|
|
String,
|
|
|
|
/// An instant of time that recurs every day
|
|
Time,
|
|
|
|
/// A user-defined datatype
|
|
UserDefined(String),
|
|
|
|
/// A non-standard datatype
|
|
Other {
|
|
/// The prefix of the specified datatype. Should be registered with the XMPP Registrar.
|
|
prefix: String,
|
|
/// The actual value of the specified datatype. E.g. "lat" in the case of "geo:lat".
|
|
value: String,
|
|
},
|
|
}
|
|
|
|
/// Validation rules for a DataForms Field.
|
|
#[derive(FromXml, AsXml, Debug, Clone, PartialEq)]
|
|
#[xml(namespace = ns::XDATA_VALIDATE, name = "validate")]
|
|
pub struct Validate {
|
|
/// The 'datatype' attribute specifies the datatype. This attribute is OPTIONAL, and defaults
|
|
/// to "xs:string". It MUST meet one of the following conditions:
|
|
///
|
|
/// - Start with "xs:", and be one of the "built-in" datatypes defined in XML Schema Part 2
|
|
/// - Start with a prefix registered with the XMPP Registrar
|
|
/// - Start with "x:", and specify a user-defined datatype.
|
|
///
|
|
/// Note that while "x:" allows for ad-hoc definitions, its use is NOT RECOMMENDED.
|
|
#[xml(attribute(default))]
|
|
pub datatype: Option<Datatype>,
|
|
|
|
/// The validation method. If no validation method is specified, form processors MUST
|
|
/// assume `<basic/>` validation. The `<validate/>` element SHOULD include one of the above
|
|
/// validation method elements, and MUST NOT include more than one.
|
|
///
|
|
/// Any validation method applied to a field of type "list-multi", "list-single", or "text-multi"
|
|
/// (other than `<basic/>`) MUST imply the same behavior as `<open/>`, with the additional constraints
|
|
/// defined by that method.
|
|
///
|
|
/// <https://xmpp.org/extensions/xep-0122.html#usecases-validation>
|
|
#[xml(child(default))]
|
|
pub method: Option<Method>,
|
|
|
|
/// For "list-multi", validation can indicate (via the `<list-range/>` element) that a minimum
|
|
/// and maximum number of options should be selected and/or entered. This selection range
|
|
/// MAY be combined with the other methods to provide more flexibility.
|
|
/// The `<list-range/>` element SHOULD be included only when the `<field/>` is of type "list-multi"
|
|
/// and SHOULD be ignored otherwise.
|
|
///
|
|
/// The `<list-range/>` element SHOULD possess either a 'min' or 'max' attribute, and MAY possess
|
|
/// both. If neither attribute is included, the processor MUST assume that there are no
|
|
/// selection constraints.
|
|
///
|
|
/// <https://xmpp.org/extensions/xep-0122.html#usecases-ranges>
|
|
#[xml(child(default))]
|
|
pub list_range: Option<ListRange>,
|
|
}
|
|
|
|
impl FromStr for Datatype {
|
|
type Err = DatatypeError;
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
let mut parts = s.splitn(2, ":");
|
|
|
|
let Some(prefix) = parts.next() else {
|
|
return Err(DatatypeError::MissingPrefix {
|
|
input: s.to_string(),
|
|
});
|
|
};
|
|
|
|
match prefix {
|
|
"xs" => (),
|
|
"x" => {
|
|
return Ok(Datatype::UserDefined(
|
|
parts.next().unwrap_or_default().to_string(),
|
|
))
|
|
}
|
|
_ => {
|
|
return Ok(Datatype::Other {
|
|
prefix: prefix.to_string(),
|
|
value: parts.next().unwrap_or_default().to_string(),
|
|
})
|
|
}
|
|
}
|
|
|
|
let Some(datatype) = parts.next() else {
|
|
return Err(DatatypeError::InvalidType {
|
|
input: s.to_string(),
|
|
});
|
|
};
|
|
|
|
let parsed_datatype = match datatype {
|
|
"anyURI" => Datatype::AnyUri,
|
|
"byte" => Datatype::Byte,
|
|
"date" => Datatype::Date,
|
|
"dateTime" => Datatype::DateTime,
|
|
"decimal" => Datatype::Decimal,
|
|
"double" => Datatype::Double,
|
|
"int" => Datatype::Int,
|
|
"integer" => Datatype::Integer,
|
|
"language" => Datatype::Language,
|
|
"long" => Datatype::Long,
|
|
"short" => Datatype::Short,
|
|
"string" => Datatype::String,
|
|
"time" => Datatype::Time,
|
|
_ => {
|
|
return Err(DatatypeError::UnknownType {
|
|
input: s.to_string(),
|
|
})
|
|
}
|
|
};
|
|
|
|
Ok(parsed_datatype)
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for Datatype {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
let value = match self {
|
|
Datatype::AnyUri => "xs:anyURI",
|
|
Datatype::Byte => "xs:byte",
|
|
Datatype::Date => "xs:date",
|
|
Datatype::DateTime => "xs:dateTime",
|
|
Datatype::Decimal => "xs:decimal",
|
|
Datatype::Double => "xs:double",
|
|
Datatype::Int => "xs:int",
|
|
Datatype::Integer => "xs:integer",
|
|
Datatype::Language => "xs:language",
|
|
Datatype::Long => "xs:long",
|
|
Datatype::Short => "xs:short",
|
|
Datatype::String => "xs:string",
|
|
Datatype::Time => "xs:time",
|
|
Datatype::UserDefined(value) => return write!(f, "x:{value}"),
|
|
Datatype::Other { prefix, value } => return write!(f, "{prefix}:{value}"),
|
|
};
|
|
f.write_str(value)
|
|
}
|
|
}
|
|
|
|
impl IntoAttributeValue for Datatype {
|
|
fn into_attribute_value(self) -> Option<String> {
|
|
Some(self.to_string())
|
|
}
|
|
}
|
|
|
|
impl FromXmlText for Datatype {
|
|
fn from_xml_text(s: String) -> Result<Datatype, Error> {
|
|
s.parse().map_err(Error::text_parse_error)
|
|
}
|
|
}
|
|
|
|
impl AsXmlText for Datatype {
|
|
fn as_xml_text(&self) -> Result<Cow<'_, str>, Error> {
|
|
Ok(Cow::Owned(self.to_string()))
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use minidom::Element;
|
|
|
|
#[test]
|
|
fn test_parse_datatype() -> Result<(), DatatypeError> {
|
|
assert_eq!(Datatype::AnyUri, "xs:anyURI".parse()?);
|
|
assert_eq!(
|
|
Err(DatatypeError::UnknownType {
|
|
input: "xs:anyuri".to_string()
|
|
}),
|
|
"xs:anyuri".parse::<Datatype>(),
|
|
);
|
|
assert_eq!(
|
|
"xs:".parse::<Datatype>(),
|
|
Err(DatatypeError::UnknownType {
|
|
input: "xs:".to_string()
|
|
})
|
|
);
|
|
assert_eq!(
|
|
Datatype::AnyUri.into_attribute_value(),
|
|
Some("xs:anyURI".to_string())
|
|
);
|
|
|
|
assert_eq!(Datatype::UserDefined("id".to_string()), "x:id".parse()?);
|
|
assert_eq!(Datatype::UserDefined("".to_string()), "x:".parse()?);
|
|
assert_eq!(
|
|
Datatype::UserDefined("id".to_string()).into_attribute_value(),
|
|
Some("x:id".to_string())
|
|
);
|
|
|
|
assert_eq!(
|
|
Datatype::Other {
|
|
prefix: "geo".to_string(),
|
|
value: "lat".to_string()
|
|
},
|
|
"geo:lat".parse()?
|
|
);
|
|
assert_eq!(
|
|
Datatype::Other {
|
|
prefix: "geo".to_string(),
|
|
value: "".to_string()
|
|
},
|
|
"geo:".parse()?
|
|
);
|
|
assert_eq!(
|
|
Datatype::Other {
|
|
prefix: "geo".to_string(),
|
|
value: "lat".to_string()
|
|
}
|
|
.into_attribute_value(),
|
|
Some("geo:lat".to_string())
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_validate_element() -> Result<(), Error> {
|
|
let cases = [
|
|
(
|
|
r#"<validate xmlns='http://jabber.org/protocol/xdata-validate'/>"#,
|
|
Validate {
|
|
datatype: None,
|
|
method: None,
|
|
list_range: None,
|
|
},
|
|
),
|
|
(
|
|
r#"<validate xmlns='http://jabber.org/protocol/xdata-validate' datatype='xs:string'><basic/><list-range max='3' min='1'/></validate>"#,
|
|
Validate {
|
|
datatype: Some(Datatype::String),
|
|
method: Some(Method::Basic),
|
|
list_range: Some(ListRange {
|
|
min: Some(1),
|
|
max: Some(3),
|
|
}),
|
|
},
|
|
),
|
|
(
|
|
r#"<validate xmlns='http://jabber.org/protocol/xdata-validate' datatype='xs:string'><regex>([0-9]{3})-([0-9]{2})-([0-9]{4})</regex></validate>"#,
|
|
Validate {
|
|
datatype: Some(Datatype::String),
|
|
method: Some(Method::Regex(
|
|
"([0-9]{3})-([0-9]{2})-([0-9]{4})".to_string(),
|
|
)),
|
|
list_range: None,
|
|
},
|
|
),
|
|
(
|
|
r#"<validate xmlns='http://jabber.org/protocol/xdata-validate' datatype='xs:dateTime'><range max='2003-10-24T23:59:59-07:00' min='2003-10-05T00:00:00-07:00'/></validate>"#,
|
|
Validate {
|
|
datatype: Some(Datatype::DateTime),
|
|
method: Some(Method::Range {
|
|
min: Some("2003-10-05T00:00:00-07:00".to_string()),
|
|
max: Some("2003-10-24T23:59:59-07:00".to_string()),
|
|
}),
|
|
list_range: None,
|
|
},
|
|
),
|
|
];
|
|
|
|
for case in cases {
|
|
let parsed_element: Validate = case
|
|
.0
|
|
.parse::<Element>()
|
|
.expect(&format!("Failed to parse {}", case.0))
|
|
.try_into()?;
|
|
|
|
assert_eq!(parsed_element, case.1);
|
|
|
|
let xml = String::from(&Element::from(parsed_element));
|
|
assert_eq!(xml, case.0);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
#[cfg_attr(
|
|
feature = "disable-validation",
|
|
should_panic = "Validate::try_from(element).is_err()"
|
|
)]
|
|
fn test_fails_with_invalid_children() {
|
|
let cases = [
|
|
r#"<validate xmlns='http://jabber.org/protocol/xdata-validate'><basic /><open /></validate>"#,
|
|
r#"<validate xmlns='http://jabber.org/protocol/xdata-validate'><unknown /></validate>"#,
|
|
];
|
|
|
|
for case in cases {
|
|
let element = case
|
|
.parse::<Element>()
|
|
.expect(&format!("Failed to parse {}", case));
|
|
assert!(Validate::try_from(element).is_err());
|
|
}
|
|
}
|
|
}
|