data_forms_validate.rs

  1// Copyright (c) 2024 xmpp-rs contributors.
  2//
  3// This Source Code Form is subject to the terms of the Mozilla Public
  4// License, v. 2.0. If a copy of the MPL was not distributed with this
  5// file, You can obtain one at http://mozilla.org/MPL/2.0/.
  6
  7use std::fmt::{Display, Formatter};
  8use std::str::FromStr;
  9
 10use minidom::{Element, IntoAttributeValue};
 11use xso::{
 12    error::{Error, FromElementError},
 13    AsXml, FromXml,
 14};
 15
 16use crate::ns::{self, XDATA_VALIDATE};
 17
 18/// Validation Method
 19#[derive(Debug, Clone, PartialEq)]
 20pub enum Method {
 21    /// … to indicate that the value(s) should simply match the field type and datatype constraints,
 22    /// the `<validate/>` element shall contain a `<basic/>` child element. Using `<basic/>` validation,
 23    /// the form interpreter MUST follow the validation rules of the datatype (if understood) and
 24    /// the field type.
 25    ///
 26    /// <https://xmpp.org/extensions/xep-0122.html#usercases-validation.basic>
 27    Basic,
 28
 29    /// For "list-single" or "list-multi", to indicate that the user may enter a custom value
 30    /// (matching the datatype constraints) or choose from the predefined values, the `<validate/>`
 31    /// element shall contain an `<open/>` child element. The `<open/>` validation method applies to
 32    /// "text-multi" differently; it hints that each value for a "text-multi" field shall be
 33    /// validated separately. This effectively turns "text-multi" fields into an open-ended
 34    /// "list-multi", with no options and all values automatically selected.
 35    ///
 36    /// <https://xmpp.org/extensions/xep-0122.html#usercases-validation.open>
 37    Open,
 38
 39    /// To indicate that the value should fall within a certain range, the `<validate/>` element shall
 40    /// contain a `<range/>` child element. The 'min' and 'max' attributes of the `<range/>` element
 41    /// specify the minimum and maximum values allowed, respectively.
 42    ///
 43    /// The 'max' attribute specifies the maximum allowable value. This attribute is OPTIONAL.
 44    /// The value depends on the datatype in use.
 45    ///
 46    /// The 'min' attribute specifies the minimum allowable value. This attribute is OPTIONAL.
 47    /// The value depends on the datatype in use.
 48    ///
 49    /// The `<range/>` element SHOULD possess either a 'min' or 'max' attribute, and MAY possess both.
 50    /// If neither attribute is included, the processor MUST assume that there are no range
 51    /// constraints.
 52    ///
 53    /// <https://xmpp.org/extensions/xep-0122.html#usercases-validation.range>
 54    Range {
 55        /// The 'min' attribute specifies the minimum allowable value.
 56        min: Option<String>,
 57        /// The 'max' attribute specifies the maximum allowable value.
 58        max: Option<String>,
 59    },
 60
 61    /// To indicate that the value should be restricted to a regular expression, the `<validate/>`
 62    /// element shall contain a `<regex/>` child element. The XML character data of this element is
 63    /// the pattern to apply. The syntax of this content MUST be that defined for POSIX extended
 64    /// regular expressions, including support for Unicode. The `<regex/>` element MUST contain
 65    /// character data only.
 66    ///
 67    /// <https://xmpp.org/extensions/xep-0122.html#usercases-validatoin.regex>
 68    Regex(String),
 69}
 70
 71/// Selection Ranges in "list-multi"
 72#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
 73#[xml(namespace = ns::XDATA_VALIDATE, name = "list-range")]
 74pub struct ListRange {
 75    /// The 'min' attribute specifies the minimum allowable number of selected/entered values.
 76    #[xml(attribute(default))]
 77    pub min: Option<u32>,
 78    /// The 'max' attribute specifies the maximum allowable number of selected/entered values.
 79    #[xml(attribute(default))]
 80    pub max: Option<u32>,
 81}
 82
 83/// Enum representing errors that can occur while parsing a `Datatype`.
 84#[derive(Debug, Clone, PartialEq)]
 85pub enum DatatypeError {
 86    /// Error indicating that a prefix is missing in the validation datatype.
 87    MissingPrefix {
 88        /// The invalid string that caused this error.
 89        input: String,
 90    },
 91
 92    /// Error indicating that the validation datatype is invalid.
 93    InvalidType {
 94        /// The invalid string that caused this error.
 95        input: String,
 96    },
 97
 98    /// Error indicating that the validation datatype is unknown.
 99    UnknownType {
100        /// The invalid string that caused this error.
101        input: String,
102    },
103}
104
105impl std::error::Error for DatatypeError {}
106
107impl Display for DatatypeError {
108    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
109        match self {
110            DatatypeError::MissingPrefix { input } => {
111                write!(f, "Missing prefix in validation datatype {input:?}.")
112            }
113            DatatypeError::InvalidType { input } => {
114                write!(f, "Invalid validation datatype {input:?}.")
115            }
116            DatatypeError::UnknownType { input } => {
117                write!(f, "Unknown validation datatype {input:?}.")
118            }
119        }
120    }
121}
122
123/// Data Forms Validation Datatypes
124///
125/// <https://xmpp.org/registrar/xdv-datatypes.html>
126#[derive(Debug, Clone, PartialEq)]
127pub enum Datatype {
128    /// A Uniform Resource Identifier Reference (URI)
129    AnyUri,
130
131    /// An integer with the specified min/max
132    /// Min: -128, Max: 127
133    Byte,
134
135    /// A calendar date
136    Date,
137
138    /// A specific instant of time
139    DateTime,
140
141    /// An arbitrary-precision decimal number
142    Decimal,
143
144    /// An IEEE double-precision 64-bit floating point type
145    Double,
146
147    /// An integer with the specified min/max
148    /// Min: -2147483648, Max: 2147483647
149    Int,
150
151    /// A decimal number with no fraction digits
152    Integer,
153
154    /// A language identifier as defined by RFC 1766
155    Language,
156
157    /// An integer with the specified min/max
158    /// Min: -9223372036854775808, Max: 9223372036854775807
159    Long,
160
161    /// An integer with the specified min/max
162    /// Min: -32768, Max: 32767
163    Short,
164
165    /// A character strings in XML
166    String,
167
168    /// An instant of time that recurs every day
169    Time,
170
171    /// A user-defined datatype
172    UserDefined(String),
173
174    /// A non-standard datatype
175    Other {
176        /// The prefix of the specified datatype. Should be registered with the XMPP Registrar.
177        prefix: String,
178        /// The actual value of the specified datatype. E.g. "lat" in the case of "geo:lat".
179        value: String,
180    },
181}
182
183/// Validation rules for a DataForms Field.
184#[derive(Debug, Clone, PartialEq)]
185pub struct Validate {
186    /// The 'datatype' attribute specifies the datatype. This attribute is OPTIONAL, and defaults
187    /// to "xs:string". It MUST meet one of the following conditions:
188    ///
189    /// - Start with "xs:", and be one of the "built-in" datatypes defined in XML Schema Part 2
190    /// - Start with a prefix registered with the XMPP Registrar
191    /// - Start with "x:", and specify a user-defined datatype.
192    ///
193    /// Note that while "x:" allows for ad-hoc definitions, its use is NOT RECOMMENDED.
194    pub datatype: Option<Datatype>,
195
196    /// The validation method. If no validation method is specified, form processors MUST
197    /// assume `<basic/>` validation. The `<validate/>` element SHOULD include one of the above
198    /// validation method elements, and MUST NOT include more than one.
199    ///
200    /// Any validation method applied to a field of type "list-multi", "list-single", or "text-multi"
201    /// (other than `<basic/>`) MUST imply the same behavior as `<open/>`, with the additional constraints
202    /// defined by that method.
203    ///
204    /// <https://xmpp.org/extensions/xep-0122.html#usecases-validation>
205    pub method: Option<Method>,
206
207    /// For "list-multi", validation can indicate (via the `<list-range/>` element) that a minimum
208    /// and maximum number of options should be selected and/or entered. This selection range
209    /// MAY be combined with the other methods to provide more flexibility.
210    /// The `<list-range/>` element SHOULD be included only when the `<field/>` is of type "list-multi"
211    /// and SHOULD be ignored otherwise.
212    ///
213    /// The `<list-range/>` element SHOULD possess either a 'min' or 'max' attribute, and MAY possess
214    /// both. If neither attribute is included, the processor MUST assume that there are no
215    /// selection constraints.
216    ///
217    /// <https://xmpp.org/extensions/xep-0122.html#usecases-ranges>
218    pub list_range: Option<ListRange>,
219}
220
221impl TryFrom<Element> for Validate {
222    type Error = FromElementError;
223
224    fn try_from(elem: Element) -> Result<Self, Self::Error> {
225        check_self!(elem, "validate", XDATA_VALIDATE);
226        check_no_unknown_attributes!(elem, "item", ["datatype"]);
227
228        let mut validate = Validate {
229            datatype: elem
230                .attr("datatype")
231                .map(Datatype::from_str)
232                .transpose()
233                .map_err(|err| FromElementError::Invalid(Error::TextParseError(err.into())))?,
234            method: None,
235            list_range: None,
236        };
237
238        for child in elem.children() {
239            match child {
240                _ if child.is("list-range", XDATA_VALIDATE) => {
241                    let list_range = ListRange::try_from(child.clone())?;
242                    if validate.list_range.is_some() {
243                        return Err(Error::Other(
244                            "Encountered unsupported number (n > 1) of list-range in validate element.",
245                        ).into());
246                    }
247                    validate.list_range = Some(list_range);
248                }
249                _ => {
250                    let method = Method::try_from(child.clone())?;
251                    if validate.method.is_some() {
252                        return Err(Error::Other(
253                            "Encountered unsupported number (n > 1) of validation methods in validate element.",
254                        ).into());
255                    }
256                    validate.method = Some(method);
257                }
258            }
259        }
260
261        Ok(validate)
262    }
263}
264
265impl From<Validate> for Element {
266    fn from(value: Validate) -> Self {
267        Element::builder("validate", XDATA_VALIDATE)
268            .attr("datatype", value.datatype)
269            .append_all(value.method)
270            .append_all(value.list_range)
271            .build()
272    }
273}
274
275impl TryFrom<Element> for Method {
276    type Error = Error;
277
278    fn try_from(elem: Element) -> Result<Self, Self::Error> {
279        let method = match elem {
280            _ if elem.is("basic", XDATA_VALIDATE) => {
281                check_no_attributes!(elem, "basic");
282                Method::Basic
283            }
284            _ if elem.is("open", XDATA_VALIDATE) => {
285                check_no_attributes!(elem, "open");
286                Method::Open
287            }
288            _ if elem.is("range", XDATA_VALIDATE) => {
289                check_no_unknown_attributes!(elem, "range", ["min", "max"]);
290                Method::Range {
291                    min: elem.attr("min").map(ToString::to_string),
292                    max: elem.attr("max").map(ToString::to_string),
293                }
294            }
295            _ if elem.is("regex", XDATA_VALIDATE) => {
296                check_no_attributes!(elem, "regex");
297                check_no_children!(elem, "regex");
298                Method::Regex(elem.text())
299            }
300            _ => return Err(Error::Other("Encountered invalid validation method.")),
301        };
302        Ok(method)
303    }
304}
305
306impl From<Method> for Element {
307    fn from(value: Method) -> Self {
308        match value {
309            Method::Basic => Element::builder("basic", XDATA_VALIDATE),
310            Method::Open => Element::builder("open", XDATA_VALIDATE),
311            Method::Range { min, max } => Element::builder("range", XDATA_VALIDATE)
312                .attr("min", min)
313                .attr("max", max),
314            Method::Regex(regex) => Element::builder("regex", XDATA_VALIDATE).append(regex),
315        }
316        .build()
317    }
318}
319
320impl FromStr for Datatype {
321    type Err = DatatypeError;
322
323    fn from_str(s: &str) -> Result<Self, Self::Err> {
324        let mut parts = s.splitn(2, ":");
325
326        let Some(prefix) = parts.next() else {
327            return Err(DatatypeError::MissingPrefix {
328                input: s.to_string(),
329            });
330        };
331
332        match prefix {
333            "xs" => (),
334            "x" => {
335                return Ok(Datatype::UserDefined(
336                    parts.next().unwrap_or_default().to_string(),
337                ))
338            }
339            _ => {
340                return Ok(Datatype::Other {
341                    prefix: prefix.to_string(),
342                    value: parts.next().unwrap_or_default().to_string(),
343                })
344            }
345        }
346
347        let Some(datatype) = parts.next() else {
348            return Err(DatatypeError::InvalidType {
349                input: s.to_string(),
350            });
351        };
352
353        let parsed_datatype = match datatype {
354            "anyURI" => Datatype::AnyUri,
355            "byte" => Datatype::Byte,
356            "date" => Datatype::Date,
357            "dateTime" => Datatype::DateTime,
358            "decimal" => Datatype::Decimal,
359            "double" => Datatype::Double,
360            "int" => Datatype::Int,
361            "integer" => Datatype::Integer,
362            "language" => Datatype::Language,
363            "long" => Datatype::Long,
364            "short" => Datatype::Short,
365            "string" => Datatype::String,
366            "time" => Datatype::Time,
367            _ => {
368                return Err(DatatypeError::UnknownType {
369                    input: s.to_string(),
370                })
371            }
372        };
373
374        Ok(parsed_datatype)
375    }
376}
377
378impl Display for Datatype {
379    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
380        let value = match self {
381            Datatype::AnyUri => "xs:anyURI",
382            Datatype::Byte => "xs:byte",
383            Datatype::Date => "xs:date",
384            Datatype::DateTime => "xs:dateTime",
385            Datatype::Decimal => "xs:decimal",
386            Datatype::Double => "xs:double",
387            Datatype::Int => "xs:int",
388            Datatype::Integer => "xs:integer",
389            Datatype::Language => "xs:language",
390            Datatype::Long => "xs:long",
391            Datatype::Short => "xs:short",
392            Datatype::String => "xs:string",
393            Datatype::Time => "xs:time",
394            Datatype::UserDefined(value) => return write!(f, "x:{value}"),
395            Datatype::Other { prefix, value } => return write!(f, "{prefix}:{value}"),
396        };
397        f.write_str(value)
398    }
399}
400
401impl IntoAttributeValue for Datatype {
402    fn into_attribute_value(self) -> Option<String> {
403        Some(self.to_string())
404    }
405}
406
407#[cfg(test)]
408mod tests {
409    use super::*;
410
411    #[test]
412    fn test_parse_datatype() -> Result<(), DatatypeError> {
413        assert_eq!(Datatype::AnyUri, "xs:anyURI".parse()?);
414        assert_eq!(
415            Err(DatatypeError::UnknownType {
416                input: "xs:anyuri".to_string()
417            }),
418            "xs:anyuri".parse::<Datatype>(),
419        );
420        assert_eq!(
421            "xs:".parse::<Datatype>(),
422            Err(DatatypeError::UnknownType {
423                input: "xs:".to_string()
424            })
425        );
426        assert_eq!(
427            Datatype::AnyUri.into_attribute_value(),
428            Some("xs:anyURI".to_string())
429        );
430
431        assert_eq!(Datatype::UserDefined("id".to_string()), "x:id".parse()?);
432        assert_eq!(Datatype::UserDefined("".to_string()), "x:".parse()?);
433        assert_eq!(
434            Datatype::UserDefined("id".to_string()).into_attribute_value(),
435            Some("x:id".to_string())
436        );
437
438        assert_eq!(
439            Datatype::Other {
440                prefix: "geo".to_string(),
441                value: "lat".to_string()
442            },
443            "geo:lat".parse()?
444        );
445        assert_eq!(
446            Datatype::Other {
447                prefix: "geo".to_string(),
448                value: "".to_string()
449            },
450            "geo:".parse()?
451        );
452        assert_eq!(
453            Datatype::Other {
454                prefix: "geo".to_string(),
455                value: "lat".to_string()
456            }
457            .into_attribute_value(),
458            Some("geo:lat".to_string())
459        );
460
461        Ok(())
462    }
463
464    #[test]
465    fn test_parse_validate_element() -> Result<(), Error> {
466        let cases = [
467            (
468                r#"<validate xmlns='http://jabber.org/protocol/xdata-validate'/>"#,
469                Validate {
470                    datatype: None,
471                    method: None,
472                    list_range: None,
473                },
474            ),
475            (
476                r#"<validate xmlns='http://jabber.org/protocol/xdata-validate' datatype="xs:string"><basic/><list-range max="3" min="1"/></validate>"#,
477                Validate {
478                    datatype: Some(Datatype::String),
479                    method: Some(Method::Basic),
480                    list_range: Some(ListRange {
481                        min: Some(1),
482                        max: Some(3),
483                    }),
484                },
485            ),
486            (
487                r#"<validate xmlns='http://jabber.org/protocol/xdata-validate' datatype="xs:string"><regex>([0-9]{3})-([0-9]{2})-([0-9]{4})</regex></validate>"#,
488                Validate {
489                    datatype: Some(Datatype::String),
490                    method: Some(Method::Regex(
491                        "([0-9]{3})-([0-9]{2})-([0-9]{4})".to_string(),
492                    )),
493                    list_range: None,
494                },
495            ),
496            (
497                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>"#,
498                Validate {
499                    datatype: Some(Datatype::DateTime),
500                    method: Some(Method::Range {
501                        min: Some("2003-10-05T00:00:00-07:00".to_string()),
502                        max: Some("2003-10-24T23:59:59-07:00".to_string()),
503                    }),
504                    list_range: None,
505                },
506            ),
507        ];
508
509        for case in cases {
510            let parsed_element: Validate = case
511                .0
512                .parse::<Element>()
513                .expect(&format!("Failed to parse {}", case.0))
514                .try_into()?;
515
516            assert_eq!(parsed_element, case.1);
517
518            let xml = String::from(&Element::from(parsed_element));
519            assert_eq!(xml, case.0);
520        }
521
522        Ok(())
523    }
524
525    #[test]
526    fn test_fails_with_invalid_children() -> Result<(), Error> {
527        let cases = [
528            r#"<validate xmlns='http://jabber.org/protocol/xdata-validate'><basic /><open /></validate>"#,
529            r#"<validate xmlns='http://jabber.org/protocol/xdata-validate'><unknown /></validate>"#,
530        ];
531
532        for case in cases {
533            let element = case
534                .parse::<Element>()
535                .expect(&format!("Failed to parse {}", case));
536            assert!(Validate::try_from(element).is_err());
537        }
538
539        Ok(())
540    }
541}