xmpp-parsers-0.22.0/.cargo_vcs_info.json0000644000000001450000000000100135560ustar { "git": { "sha1": "0e7ac066d4d496f0e49887bc8d2a333e38cdd3c5" }, "path_in_vcs": "parsers" }xmpp-parsers-0.22.0/Cargo.lock0000644000000520740000000000100115410ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 4 [[package]] name = "autocfg" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "blake2" version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" dependencies = [ "digest", ] [[package]] name = "block-buffer" version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ "generic-array", ] [[package]] name = "bumpalo" version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "bytes" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "castaway" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" dependencies = [ "rustversion", ] [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "chrono" version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" dependencies = [ "num-traits", ] [[package]] name = "compact_str" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f" dependencies = [ "castaway", "cfg-if", "itoa", "ryu", "static_assertions", ] [[package]] name = "cpufeatures" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ "libc", ] [[package]] name = "crypto-common" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", "typenum", ] [[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", "subtle", ] [[package]] name = "displaydoc" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "futures-core" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "generic-array" version = "0.14.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" dependencies = [ "typenum", "version_check", ] [[package]] name = "getrandom" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", "r-efi", "wasip2", ] [[package]] name = "icu_collections" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" dependencies = [ "displaydoc", "potential_utf", "yoke", "zerofrom", "zerovec", ] [[package]] name = "icu_locale_core" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" dependencies = [ "displaydoc", "litemap", "tinystr", "writeable", "zerovec", ] [[package]] name = "icu_normalizer" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" dependencies = [ "icu_collections", "icu_normalizer_data", "icu_properties", "icu_provider", "smallvec", "zerovec", ] [[package]] name = "icu_normalizer_data" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" dependencies = [ "icu_collections", "icu_locale_core", "icu_properties_data", "icu_provider", "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" [[package]] name = "icu_provider" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" dependencies = [ "displaydoc", "icu_locale_core", "writeable", "yoke", "zerofrom", "zerotrie", "zerovec", ] [[package]] name = "idna" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", "utf8_iter", ] [[package]] name = "idna_adapter" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" dependencies = [ "icu_normalizer", "icu_properties", ] [[package]] name = "itoa" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jid" version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50fe7c84851804789723602f7a6f8e3420993d9dc465218cd202e673e44d12cc" dependencies = [ "idna", "memchr", "minidom 0.16.0", "serde", "stringprep", ] [[package]] name = "js-sys" version = "0.3.82" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" dependencies = [ "once_cell", "wasm-bindgen", ] [[package]] name = "keccak" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" dependencies = [ "cpufeatures", ] [[package]] name = "libc" version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" [[package]] name = "litemap" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] name = "log" version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" [[package]] name = "memchr" version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "minidom" version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e394a0e3c7ccc2daea3dffabe82f09857b6b510cb25af87d54bf3e910ac1642d" dependencies = [ "rxml 0.11.1", ] [[package]] name = "minidom" version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "040b4e38f8abd85ddf9ea00646accc716b9d15e283b0aacb16f697ddba432c97" dependencies = [ "rxml 0.13.3", "thiserror", ] [[package]] name = "num-traits" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] [[package]] name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "potential_utf" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" dependencies = [ "zerovec", ] [[package]] name = "proc-macro2" version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" dependencies = [ "proc-macro2", ] [[package]] name = "r-efi" version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "rustversion" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "rxml" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65bc94b580d0f5a6b7a2d604e597513d3c673154b52ddeccd1d5c32360d945ee" dependencies = [ "bytes", "rxml_validation", ] [[package]] name = "rxml" version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f288457ad3607c08953ca5604229ebb03878a0b4491d8f545d547281c2b3e0c5" dependencies = [ "bytes", "futures-core", "rxml_proc", "rxml_validation", ] [[package]] name = "rxml_proc" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d46a2e263a1beb8b39e69bffee9f2395f479cc02472c247af00e065006970ca7" dependencies = [ "quote", "rxml_validation", "syn", ] [[package]] name = "rxml_validation" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "826e80413b9a35e9d33217b3dcac04cf95f6559d15944b93887a08be5496c4a4" dependencies = [ "compact_str", ] [[package]] name = "ryu" version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "serde" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", "serde_derive", ] [[package]] name = "serde_core" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_json" version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ "itoa", "memchr", "ryu", "serde", "serde_core", ] [[package]] name = "sha1" version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures", "digest", ] [[package]] name = "sha2" version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures", "digest", ] [[package]] name = "sha3" version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" dependencies = [ "digest", "keccak", ] [[package]] name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "stable_deref_trait" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "static_assertions" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "stringprep" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" dependencies = [ "unicode-bidi", "unicode-normalization", "unicode-properties", ] [[package]] name = "subtle" version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" version = "2.0.108" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "synstructure" version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "thiserror" version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "tinystr" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ "displaydoc", "zerovec", ] [[package]] name = "tinyvec" version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" dependencies = [ "tinyvec_macros", ] [[package]] name = "tinyvec_macros" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "typenum" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "unicode-bidi" version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-ident" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "unicode-normalization" version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" dependencies = [ "tinyvec", ] [[package]] name = "unicode-properties" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" [[package]] name = "utf8_iter" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" dependencies = [ "getrandom", "js-sys", "wasm-bindgen", ] [[package]] name = "version_check" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "wasip2" version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ "wit-bindgen", ] [[package]] name = "wasm-bindgen" version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" dependencies = [ "bumpalo", "proc-macro2", "quote", "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" dependencies = [ "unicode-ident", ] [[package]] name = "wit-bindgen" version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "writeable" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] name = "xmpp-parsers" version = "0.22.0" dependencies = [ "base64", "blake2", "chrono", "digest", "jid", "log", "minidom 0.18.0", "serde_json", "sha1", "sha2", "sha3", "uuid", "xso", ] [[package]] name = "xso" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71699b74d769a35bd2b1dad02a3861bb4fe907d510b86230230d5d2bb1aac7c3" dependencies = [ "base64", "bytes", "jid", "minidom 0.18.0", "rxml 0.13.3", "serde_json", "uuid", "xso_proc", ] [[package]] name = "xso_proc" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3500ced69bcf662565b866595ad4a64773906a339f554ab834cfb2e10c527df9" dependencies = [ "proc-macro2", "quote", "rxml_validation", "syn", ] [[package]] name = "yoke" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" dependencies = [ "stable_deref_trait", "yoke-derive", "zerofrom", ] [[package]] name = "yoke-derive" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", "syn", "synstructure", ] [[package]] name = "zerofrom" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", "syn", "synstructure", ] [[package]] name = "zerotrie" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" dependencies = [ "displaydoc", "yoke", "zerofrom", ] [[package]] name = "zerovec" version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ "yoke", "zerofrom", "zerovec-derive", ] [[package]] name = "zerovec-derive" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", "syn", ] xmpp-parsers-0.22.0/Cargo.toml0000644000000042260000000000100115600ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" name = "xmpp-parsers" version = "0.22.0" authors = [ "Emmanuel Gil Peyrot ", "Maxime “pep” Buquet ", ] build = false autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "Collection of parsers and serialisers for XMPP extensions" homepage = "https://gitlab.com/xmpp-rs/xmpp-rs" readme = "README.md" keywords = [ "xmpp", "jabber", "xml", ] categories = [ "parsing", "network-programming", ] license = "MPL-2.0" repository = "https://gitlab.com/xmpp-rs/xmpp-rs" [package.metadata.docs.rs] rustdoc-args = [ "--sort-modules-by-appearance", "-Zunstable-options", ] [features] component = [] disable-validation = ["xso/non-pedantic"] log = ["dep:log"] serde = ["jid/serde"] [lib] name = "xmpp_parsers" path = "src/lib.rs" [[example]] name = "generate-caps" path = "examples/generate-caps.rs" [dependencies.base64] version = "0.22" [dependencies.blake2] version = "0.10.4" [dependencies.chrono] version = "0.4.5" features = ["std"] default-features = false [dependencies.digest] version = "0.10" [dependencies.jid] version = "0.12" features = ["minidom"] [dependencies.log] version = "0.4" optional = true [dependencies.minidom] version = "0.18" [dependencies.serde_json] version = "1.0" features = ["alloc"] default-features = false [dependencies.sha1] version = "0.10" [dependencies.sha2] version = "0.10" [dependencies.sha3] version = "0.10" [dependencies.uuid] version = "1.9.1" features = ["v4"] [dependencies.xso] version = "0.3" features = [ "macros", "minidom", "panicking-into-impl", "jid", "uuid", "base64", "serde_json", ] xmpp-parsers-0.22.0/Cargo.toml.orig000064400000000000000000000027341046102023000152430ustar 00000000000000[package] name = "xmpp-parsers" version = "0.22.0" authors = [ "Emmanuel Gil Peyrot ", "Maxime “pep” Buquet ", ] description = "Collection of parsers and serialisers for XMPP extensions" homepage = "https://gitlab.com/xmpp-rs/xmpp-rs" repository = "https://gitlab.com/xmpp-rs/xmpp-rs" keywords = ["xmpp", "jabber", "xml"] categories = ["parsing", "network-programming"] license = "MPL-2.0" edition = "2021" [dependencies] base64 = "0.22" digest = "0.10" sha1 = "0.10" sha2 = "0.10" sha3 = "0.10" blake2 = "0.10.4" chrono = { version = "0.4.5", default-features = false, features = ["std"] } log = { version = "0.4", optional = true } # same repository dependencies jid = { version = "0.12", path = "../jid", features = ["minidom"] } minidom = { version = "0.18", path = "../minidom" } xso = { version = "0.3", path = "../xso", features = ["macros", "minidom", "panicking-into-impl", "jid", "uuid", "base64", "serde_json"] } uuid = { version = "1.9.1", features = ["v4"] } serde_json = { version = "1.0", default-features = false, features = ["alloc"] } [features] # Build xmpp-parsers to make components instead of clients. component = [] # Disable validation of unknown attributes. disable-validation = [ "xso/non-pedantic" ] # Enable serde support in jid crate serde = [ "jid/serde" ] # Enable some additional logging in helpers log = [ "dep:log" ] [package.metadata.docs.rs] rustdoc-args = [ "--sort-modules-by-appearance", "-Zunstable-options" ] xmpp-parsers-0.22.0/ChangeLog000064400000000000000000000675631046102023000141410ustar 00000000000000Version 0.22.0: 2025-10-28 pep * Breaking - MIX (XEP-0369) has been removed, it has failed to gain any traction in the XMPP ecosystem and is actively being replaced with better alternatives based on MUC. - The `alternate_address` field of `xmpp_parsers::stanza_error::StanzaError` has been moved into the corresponding enum variants of the `xmpp_parsers::stanza_error::DefinedCondition` where it may occur. - The `xmpp_parsers::mix::Participant::jid` and `..::Mix::jid` fields are now of type `BareJid` (instead of `String`), as they should always have been. This influences various other places, such as these struct's constructors. - The rtt module has been changed to make use of xso on enums. This allows us to simplify the `Action` type to include all three possible variants directly in the enum, rather than as separate types which are then included in `Action`. Also `Action::Erase::num` is now a wrapper type around u32, which lets us implement a custom Default on it. (!416) - The cert_management module is now using `Option` instead of `bool` to check whether a child element is present or not, as this is currently implemented in xso. We might revert that change if we implement flag metas in xso before the next release. - The forwarding module now hardcodes that its child is a message in the jabber:client namespace. It previously half-followed the schema, but nothing so far uses it for anything but this message. We can always change it again once any other specification allows e.g. presences or iqs. - Module `jingle_thumnails` has been renamed to `jingle_thumbnails`. It now respect the latest version of the XEP, with the width and height being limited to 0..65535 and the media-type being optional (!475). - The bookmarks2 Conference `extensions` child is now an `Option` instead of a `Vec`, to distinguish between it being absent or empty (!472). - Replace all boolean attributes with the `bool` type, so that we can use them just like normal booleans. The following structs are removed: `pubsub::pubsub::Notify`, `bookmarks2::Autojoin`, `extdisco::Restricted`, `fast::Tls0Rtt`, `legacy_omemo::IsPreKey`, `mam::Complete`, `sm::ResumeAttr` (!476) - `cert_management::Append::no_cert_management`, `cert_management::Item::no_cert_management`, `ssm::StreamManagement::optional` and `starttls::StartTls::required` are now proper bools, instead of `Option` (!518) - `bookmarks::Conference` and `bookmarks2::Conference` use ResourcePart to store the optional nickname instead of a String (!485) - pubsub Item and pubsub#event Item are now distinct structs (!503) - `message::Message` id field is now `Option` instead of `Option` (!504) - `message_correction::Replace` id field is now Id instead of String (!504) - `Caps::hash` has been split into `Caps::hash` (the algo) and `Caps::ver` (the actual hash), to reflect the attributes (!517) - `muc::MucUser` now has an `invite` field - vCard-temp queries have been split into vcard::VCardQuery (!522) - http_upload’s Header type has been split into HeaderName, an enum containing only the name of the header, and the Header struct with both a name and a value (!530) - `pubsub::Event` is now the wrapper for the `pubsub::event::Payload` enum, and the PublishedItems and RetractedItems have been merged into the Items sub-struct. These replace the previous PubSubEvent enum (!531) - `Handshake::from_password_and_stream_id()` became `Handshake::from_stream_id_and_password()`, with the two parameters having been exchanged, and the stream_id is now a String instead of &str. - `pubsub::Owner` is now a wrapper for the `pubsub::owner::Paylolad` enum, and all its direct children have been merged into this enum (!532) - `time::TimeResult` has been ported to use xso. Use From/Into to convert it to/from `chrono::DateTime` values. The numbered member `0` does not exist anymore (!551). - `stanza_error::StanzaError` has been ported to use xso (!552). - data_forms has been ported to use xso. This introduces a couple subtle breaking changes for `data_forms::DataForm`, in particular: - the `from_type` member was replaced by a pair of functions. - if the form contains a FORM_TYPE field, it will be part of the `fields` member (unlike before, where that field was removed). - `message::Message` has been ported to use xso (!560). This causes several breaking changes: 1. the `message::Body` and `message::Subject` newtypes have been removed and replaced by bare Strings 2. a `message::Lang` newtype has been introduced for the language of message parts. 3. the `message::Thread` type was converted from a single-element tuple-like struct to a struct with named fields to support the optional `parent` attribute. - `presence::Presence` has been ported to use xso (!477). It also uses the `message::Lang` newtype since !561. - `iq::Iq` has been ported to use xso (!572). That entails a couple breaking changes: - The IqType type has been removed and loosely replaced by the IqPayload type. - The Iq type is now an enum. Access to `from`, `to` and `id` needs to happen via the provided accessor functions, by `match`-ing the enum variant or by splitting the Iq into its IqHeader and IqPayload using the `split()` method. - The inverse is possible through the `assemble()` methods on IqPayload, IqHeader and Iq, whichever is more convenient. * New parsers/serialisers: - Stream Features (RFC 6120) (!400) - Spam Reporting (XEP-0377) (!506) - Extensible SASL Profile (XEP-0388) - SASL Channel-Binding Type Capability (XEP-0440) - Stream Limits Advertisement (XEP-0478) - Message Displayed Synchronization (XEP-0490) - RFC 6120 stream errors and starttls nonzas - XEP-0045 mediated invites - Push Notifications (XEP-0357) (!543) - JSON Containers (XEP-0335) (!546) - Verifying HTTP Requests via XMPP (XEP-0070) * Improvements: - Add support for undocumented ` in XEP-0198 feature advertisment, for compatibility with servers in the wild. - Add support application-specific error conditions in XEP-0198 - Keep unsupported vCard elements as `minidom::Element`, so that they get serialized back instead of being dropped. We now also test for the size of these elements (!472). - Add `Message::extract_valid_payload` method to warn in case of failure and return simply `Option` instead of `Result>` (!497) - Add `Message::get_best_body_cloned` and `Message::get_best_subject_cloned` to clone automatically when performance is not an issue (!497) - Fix compatibility to uuid 1.12 - Implement Default for Tune - Re-export `xso::error::FromElementError`. - Fix a few tests behind 'component' feature - Fix some Clippy warnings Version 0.21.0: 2024-07-25 Emmanuel Gil Peyrot * New parsers/serialisers: - Private XML Storage (XEP-0049), only enough for bookmarks. - vcard-temp (XEP-0054), only enough for the PHOTO element. - Out of Band Data (XEP-0066), only the annotation part used for file sharing in Conversations. - Data Forms Validation (XEP-0122). - vCard-Based Avatars (XEP-0153). - Jingle Content Thumbnails (XEP-264). - Bind 2 (XEP-0386). - Fast Authentication Streamlining Tokens (XEP-0484). * Improvements: - Replace many usages of our custom macros with the new xso crate, which will ultimately allow streaming directly from the rxml XML parser (in a SAX way) into our parsed structs, without going through minidom in the middle. - Re-export the jid and minidom modules, not their inner symbols. - Use edition 2021, no more use TryFrom/TryInto! \o/ - Add serde feature, passed to jid crate - Update XEP-0084 avatars to 1.1.4, which allows for avatars bigger than 64 KiB. - Make it easy to convert from bookmarks1 to bookmarks2. - Ignore the 'code' attribute on errors, deprecated with XEP-0086 in 2007 but still sent by some legacy clients or servers it seems. - Implement flip-page and metadata for XEP-0313 (Message Archive Management). - Add support for XEP-0059 (Result Set Management) for XEP-0030 disco#items. - When disable-validation is enable, stop validating XEP-0030 rules. - Make TryFrom chainable. - Add a Message::extract_payload() function. - Add a PubSubEvent::node_name() function, to simplify matching on the node name from any of the enum variants. - PubSub elements can now be compared for equality. - Ignore incorrect FORM_TYPE fields as per XEP-0068. - Allow fixed fields and desc in XEP-0004 (Data Forms). - Add constructors for DataForm and Field, to simplify their creation. - Make it possible to specify an alternate JID on and stanza errors. * Bugfixes: - Fix XEP-0257 serialisation - Fix stanza error parsing of multiple languages. - Fix all typos in the codebase found by codespell. Version 0.20.0: 2023-08-17 Maxime “pep” Buquet , Emmanuel Gil Peyrot * New parsers/serialisers: - Message Reactions (XEP-0444) * Improvements: - Update dependencies - muc::user::Item: Added with_ helpers - Correct cargo doc warnings - Presence now has constructors for each variant so you don't have to import presence::Type, where Presence::available represents type None (#79) - Presence::with_payload builds a payload into the presence (#79) - Message now has constructors for each type ; Message::new still builds a Chat type (#78) - Message::with_body builder method appends a body in a given language to the message (#78) - Derive PartialEq on Iq - impl MessagePayload for MucUser - Add MucUser::with_statuses and ::with_items * Breaking changes: - Removed the 'serde' feature. Add it directly by using 'jid'. `jid = { version = "*", features = ["serde"] }`. Version 0.19.2: 2022-12-17 Maxime “pep” Buquet * Improvements: - Derive PartialEq on Presence - impl PresencePayload for muc::user::MucUser. - New muc::user::Status::ServiceErrorKick variant for the 333 status code. - Update deprecated chrono code (FixedOffset::{east,west} to {east,west}_opt). Version 0.19.1: 2022-07-13 Emmanuel Gil Peyrot * New parsers/serialisers: - Add In-Band Real Time Text support - Add OMEMO support * Improvements: - bookmarks 2: uncomment test Version 0.19.0: 2022-03-07 Emmanuel Gil Peyrot * New parsers/serialisers: - External Service Discovery (XEP-0215). - HTTP File Upload (XEP-0363). - Message Archive Management Preferences (XEP-0441), actually spun off from XEP-0313. * Improvements: - Add constructor helpers for more structs. - Impl Eq and Hash on disco::Feature (thanks Paul!). - Impl MessagePayload for PubSubEvent (thanks Paul!). - Bump minidom to 0.14. - Bump the hash crates to their latest version. - Run clippy. - Rename the directory to parsers, to avoid colliding prefix with xmpp-rs. * Breaking changes: - Bump bookmarks 2 to version 1.1.3. - Use proper types in Jingle specs (XEP-0294 and XEP-0339). - Move preferences from MAM to MAM prefs, following the spec split. Version 0.18.1: 2021-01-13 Emmanuel Gil Peyrot * Bugfixes: - Bump minidom to 0.13, as 0.12.1 got yanked. Version 0.18.0: 2021-01-13 Emmanuel Gil Peyrot * New parsers/serialisers: - Jingle Raw UDP Transport Method (XEP-0177). - Jingle RTP Header Extensions Negotiation (XEP-0294). - Jingle Grouping Framework (XEP-0338). - Mediated Information eXchange (MIX) (XEP-0369). * Improvements: - Everything is now PartialEq! - Add "serde" feature to enable "jid/serde". - Implement more of XEP-0060. - Bump XEP-0167 to version 1.2.0, adding rtcp-mux. - Bump XEP-0176 to version 1.1, fixing interoperability with other clients. - Bump XEP-0402 to version 1.1.1, bumping its namespace and adding support for extension data. - Bump all dependencies to their latest version. - Some more helper constructors. - Make public some stuff that should have been public from the very beginning. * Bugfixes: - Jingle::set_reason() does what it says now (copy/paste error). - Bookmarks’ names are now optional like they should. Version 0.17.0: 2020-02-15 Emmanuel Gil Peyrot , Maxime “pep” Buquet , Paul Fariello * Improvements: - Add serialization tests where possible - Use minidom's NSChoice API for Jingle parser - Remove NamespaceAwareCompare. Move to minidom * Breaking changes: - Prevent generate_serializer macro from adding another layer of Node. Fixes some serializers. - ecaps2: Use the Error type instead of () Version 0.16.0: 2019-10-15 Emmanuel Gil Peyrot * New parsers/serialisers: - Client Certificate Management for SASL EXTERNAL (XEP-0257) - JID Prep (XEP-0328) - Client State Indication (XEP-0352) - OpenPGP for XMPP (XEP-0373) - Bookmarks 2 (This Time it's Serious) (XEP-0402) - Anonymous unique occupant identifiers for MUCs (XEP-0421) - Source-Specific Media Attributes in Jingle (XEP-0339) - Jingle RTP Feedback Negotiation (XEP-0293) * Breaking changes: - Presence constructors now take Into and assume Some. * Improvements: - CI: refactor, add caching - Update jid-rs to 0.8 Version 0.15.0: 2019-09-06 Emmanuel Gil Peyrot * New parsers/serialisers: - XHTML-IM (XEP-0071) - User Tune (XEP-0118) - Bits of Binary (XEP-0231) - Message Carbons (XEP-0280) * Breaking changes: - Stop reexporting TryFrom and TryInto, they are available in std::convert nowadays. - Bind has been split into BindQuery and BindResponse. * Improvements: - New DOAP file for a machine-readable description of the features. - Add various parser and formatter helpers on Hash. Version 0.14.0: 2019-07-13 Emmanuel Gil Peyrot , Maxime “pep” Buquet * New parsers/serialisers: - Entity Time (XEP-0202). * Improvements: - Microblog NS (XEP-0227). - Update jid-rs dependency with jid split change (Jid, FullJid, BareJid) and reexport them. - Fix rustdoc options in Cargo.toml for docs.rs * Breaking changes: - Presence's show attribute is now Option and Show::None is no more. Version 0.13.1: 2019-04-12 Emmanuel Gil Peyrot * Bugfixes: - Fix invalid serialisation of priority in presence. - Bump image size to u16 from u8, as per XEP-0084 version 1.1.2. * Improvements: - Drop try_from dependency, as std::convert::TryFrom got stabilised. Version 0.13.0: 2019-03-20 Emmanuel Gil Peyrot * New parsers/serialisers: - User Avatar (XEP-0084). - Contact Addresses for XMPP Services (XEP-0157). - Jingle RTP Sessions (XEP-0167). - Jingle ICE-UDP Transport Method (XEP-0176). - Use of DTLS-SRTP in Jingle Sessions (XEP-0320). * Breaking changes: - Make 'id' required on iq, as per RFC6120 §8.1.3. - Refactor PubSub to have more type-safety. - Treat FORM_TYPE as a special case in data forms, to avoid duplicating it into a field. - Add forgotten i18n to Jingle text element. * Improvements: - Add various helpers for hash representations. - Add helpers constructors for multiple extensions (disco, caps, pubsub, stanza_error). - Use Into in more constructors. - Internal change on attribute declaration in macros. - Reexport missing try_from::TryInto. Version 0.12.2: 2019-01-16 Emmanuel Gil Peyrot * Improvements: - Reexport missing util::error::Error and try_from::TryFrom. Version 0.12.1: 2019-01-16 Emmanuel Gil Peyrot * Improvements: - Reexport missing JidParseError from the jid crate. Version 0.12.0: 2019-01-16 Emmanuel Gil Peyrot * Breaking changes: - Update dependencies. - Switch to git, upstream is now available at https://gitlab.com/xmpp-rs/xmpp-parsers - Switch to Edition 2018, this removes support for rustc versions older than 1.31. - Implement support for XEP-0030 2.5rc3, relaxing the ordering of children in disco#info. * Improvements: - Test for struct size, to keep them known and avoid bloat. - Add various constructors to make the API easier to use. - Reexport Jid from the jid crate, to avoid any weird issue on using different incompatible versions of the same crate. - Add forgotten 'ask' attribute on roster item (thanks O01eg!). - Use cargo-fmt on the codebase, to lower the barrier of entry. - Add a disable-validation feature, disabling many checks xmpp-parsers is doing. This should be used for software which want to let invalid XMPP pass through instead of being rejected as invalid (thanks Astro-!). Version 0.11.1: 2018-09-20 Emmanuel Gil Peyrot * Improvements: - Document all of the modules. Version 0.11.0: 2018-08-03 Emmanuel Gil Peyrot * Breaking changes: - Split Software Version (XEP-0092) into a query and response elements. - Split RSM (XEP-0059) into a query and response elements. - Fix type safety and spec issues in RSM and MAM (XEP-0313). - Remove item@node and EmptyItems from PubSub events (XEP-0060). * Improvements: - Document many additional modules. - Add the SASL nonza, as well as the SCRAM-SHA-256 and the two -PLUS mechanisms. Version 0.10.0: 2018-07-31 Emmanuel Gil Peyrot * New parsers/serialisers: - Added , SASL and bind (RFC6120) parsers. - Added a WebSocket (RFC7395) implementation. - Added a Jabber Component (XEP-0114). - Added support for User Nickname (XEP-0172). - Added support for Stream Management (XEP-0198). - Added support for Bookmarks (XEP-0048). - Publish-Subscribe (XEP-0060) now supports requests in addition to events. * Breaking changes: - Switch from std::error to failure to report better errors. - Bump to minidom 0.9.1, and reexport minidom::Element. * Improvements: - Add getters for the best body and subject in message, to make it easier to determine which one the user wants based on their language preferences. - Add constructors and setters for most Jingle elements, to ease their creation. - Add constructors for hash, MUC item, iq and more. - Use more macros to simplify and factorise the code. - Use traits to define iq payloads. - Document more modules. Version 0.9.0: 2017-10-31 Emmanuel Gil Peyrot * New parsers/serialisers: - Blocking Command (XEP-0191) has been added. - Date and Time Profiles (XEP-0082) has been added, replacing ad-hoc use of chrono in various places. - User Mood (XEP-0107) has been added. * Breaking changes: - Fix subscription="none" not being the default. - Add more type safety to pubsub#event. - Reuse Jingle’s ContentId type in JingleFT. - Import the disposition attribute values in Jingle. * Improvements: - Refactor a good part of the code using macros. - Simplify the parsing code wherever it makes sense. - Check for children ordering in disco#info result. - Finish implementation of , and in JingleFT. - Correctly serialise , and test it. Version 0.8.0: 2017-08-27 Emmanuel Gil Peyrot * New parsers/serialisers: - iq:version (XEP-0092) has been added. - Finally implement extension serialisation in disco. * Breaking changes: - Wrap even more elements into their own type, in jingle, jingle_ft, roster, message. - Split loose enums into multiple structs where it makes sense, such as for IBB, StanzaId, Receipts. - Split disco query and answer elements into their own struct, to enforce more guarantees on both. * Improvements: - Use Vec::into_iter() more to avoid references and clones. - Make data_forms propagate a media_element error. - Document more of disco, roster, chatstates. - Use the minidom feature of jid, for IntoAttributeValue. - Add a component feature, changing the default namespace to jabber:component:accept. - Add support for indicating ranged transfers in jingle_ft. Version 0.7.1: 2017-07-24 Emmanuel Gil Peyrot * Hotfixes: - Stub out blake2 support, since the blake2 crate broke its API between their 0.6.0 and 0.6.1 releases… Version 0.7.0: 2017-07-23 Emmanuel Gil Peyrot * New parsers/serialisers: - Jingle Message Initialisation (XEP-0353) was added. - The disco#items query (XEP-0030) is now supported, in addition to the existing disco#info one. * Breaking changes: - Replaced many type aliases with proper wrapping structs. - Split Disco into a query and a result part, since they have very different constraints. - Split IqPayload in three to avoid parsing queries as results for example. * Improvements: - Use TryFrom from the try_from crate, thus removing the dependency on nightly! - Always implement From instead of Into, the latter is generated anyway. - Add helpers to construct your Presence stanza. Version 0.6.0: 2017-06-27 Emmanuel Gil Peyrot * New parsers/serialisers: - In-Band Registration (XEP-0077) was added. - Multi-User Chat (XEP-0045) got expanded a lot, thanks pep.! * Breaking changes: - Added wrappers for Strings used as identifiers, to add type safety. - Use chrono’s DateTime for JingleFT’s date element. - Use Jid for JingleS5B’s jid attribute. * Improvements: - Use more macros for common tasks. - Add a constructor for Message and Presence. - Implement std::fmt::Display and std::error::Error on our error type. - Fix DataForms serialisation. - Fix roster group serialisation. - Update libraries, notably chrono whose version 0.3.1 got yanked. Version 0.5.0: 2017-06-11 Emmanuel Gil Peyrot * New parsers/serialisers: - Implementation of the roster management protocol defined in RFC 6121 §2. - Implementation of PubSub events (except collections). - Early implementation of MUC. * Breaking changes: - Rename presence enums to make them easier to use. * Improvements: - Make hashes comparable and hashable. - Make data forms embeddable easily into minidom Element::builder. Version 0.4.0: 2017-05-28 Emmanuel Gil Peyrot * Incompatible changes: - Receipts now make the id optional, as per the specification. - Hashes now expose their raw binary value, instead of staying base64-encoded. - Parse dates (XEP-0082) in delayed delivery (XEP-0203) and last user interaction (XEP-0319), using the chrono crate. * Improvements: - Removal of most of the remaining clones, the only ones left are due to minidom not exposing a draining iterator over the children. - Finish to parse all of the attributes using get_attr!(). - More attribute checks. - Split more parsers into one parser per element. - Rely on minidom 0.4.3 to serialise more standard types automatically. - Implement forgotten serialisation for data forms (XEP-0004). - Implement legacy capabilities (XEP-0115) for compatibility with older software. Version 0.3.0: 2017-05-23 Emmanuel Gil Peyrot * Big changes: - All parsers and serialisers now consume their argument, this makes the API way more efficient, but you will have to clone before passing your structs in it if you want to keep them. - Payloads of stanzas are not parsed automatically anymore, to let applications which want to forward them as-is do so more easily. Parsing now always succeeds on unknown payloads, it just puts them into an Unknown value containing the existing minidom Element. * New parsers/serialisers: - Last User Interaction in Presence, XEP-0319. * Improved parsers/serialisers: - Message now supports subject, bodies and threads as per RFC 6121 §5.2. - Replace most attribute reads with a nice macro. - Use enums for more enum-like things, for example Algo in Hash, or FieldType in DataForm. - Wire up stanza-id and origin-id to MessagePayload. - Wire up MAM elements to message and iq payloads. - Changes in the RSM API. - Add support for more data forms elements, but still not the complete set. - Thanks to minidom 0.3.1, check for explicitly disallowed extra attributes in some elements. * Crate updates: - minidom 0.4.1 Version 0.2.0: 2017-05-06 Emmanuel Gil Peyrot * New parsers/serialisers: - Stanza error, as per RFC 6120 §8.3. - Jingle SOCKS5 Transport, XEP-0260. * Incompatible changes: - Parsers and serialisers now all implement TryFrom and Into, instead of the old parse_* and serialise_* functions. - Presence has got an overhaul, it now hosts show, statuses and priority in its struct. The status module has also been dropped. - Message now supports multiple bodies, each in a different language. The body module has also been dropped. - Iq now gets a proper StanzaError when the type is error. - Fix bogus Jingle payload, which was requiring both description and transport. * Crate updates: - minidom 0.3.0 Version 0.1.0: 2017-04-29 Emmanuel Gil Peyrot * Implement many extensions. xmpp-parsers-0.22.0/LICENSE000064400000000000000000000405261046102023000133620ustar 00000000000000Mozilla Public License Version 2.0 ================================== 1. Definitions -------------- 1.1. "Contributor" means each individual or legal entity that creates, contributes to the creation of, or owns Covered Software. 1.2. "Contributor Version" means the combination of the Contributions of others (if any) used by a Contributor and that particular Contributor's Contribution. 1.3. "Contribution" means Covered Software of a particular Contributor. 1.4. "Covered Software" means Source Code Form to which the initial Contributor has attached the notice in Exhibit A, the Executable Form of such Source Code Form, and Modifications of such Source Code Form, in each case including portions thereof. 1.5. "Incompatible With Secondary Licenses" means (a) that the initial Contributor has attached the notice described in Exhibit B to the Covered Software; or (b) that the Covered Software was made available under the terms of version 1.1 or earlier of the License, but not also under the terms of a Secondary License. 1.6. "Executable Form" means any form of the work other than Source Code Form. 1.7. "Larger Work" means a work that combines Covered Software with other material, in a separate file or files, that is not Covered Software. 1.8. "License" means this document. 1.9. "Licensable" means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently, any and all of the rights conveyed by this License. 1.10. "Modifications" means any of the following: (a) any file in Source Code Form that results from an addition to, deletion from, or modification of the contents of Covered Software; or (b) any new file in Source Code Form that contains any Covered Software. 1.11. "Patent Claims" of a Contributor means any patent claim(s), including without limitation, method, process, and apparatus claims, in any patent Licensable by such Contributor that would be infringed, but for the grant of the License, by the making, using, selling, offering for sale, having made, import, or transfer of either its Contributions or its Contributor Version. 1.12. "Secondary License" means either the GNU General Public License, Version 2.0, the GNU Lesser General Public License, Version 2.1, the GNU Affero General Public License, Version 3.0, or any later versions of those licenses. 1.13. "Source Code Form" means the form of the work preferred for making modifications. 1.14. "You" (or "Your") means an individual or a legal entity exercising rights under this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with You. For purposes of this definition, "control" means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity. 2. License Grants and Conditions -------------------------------- 2.1. Grants Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license: (a) under intellectual property rights (other than patent or trademark) Licensable by such Contributor to use, reproduce, make available, modify, display, perform, distribute, and otherwise exploit its Contributions, either on an unmodified basis, with Modifications, or as part of a Larger Work; and (b) under Patent Claims of such Contributor to make, use, sell, offer for sale, have made, import, and otherwise transfer either its Contributions or its Contributor Version. 2.2. Effective Date The licenses granted in Section 2.1 with respect to any Contribution become effective for each Contribution on the date the Contributor first distributes such Contribution. 2.3. Limitations on Grant Scope The licenses granted in this Section 2 are the only rights granted under this License. No additional rights or licenses will be implied from the distribution or licensing of Covered Software under this License. Notwithstanding Section 2.1(b) above, no patent license is granted by a Contributor: (a) for any code that a Contributor has removed from Covered Software; or (b) for infringements caused by: (i) Your and any other third party's modifications of Covered Software, or (ii) the combination of its Contributions with other software (except as part of its Contributor Version); or (c) under Patent Claims infringed by Covered Software in the absence of its Contributions. This License does not grant any rights in the trademarks, service marks, or logos of any Contributor (except as may be necessary to comply with the notice requirements in Section 3.4). 2.4. Subsequent Licenses No Contributor makes additional grants as a result of Your choice to distribute the Covered Software under a subsequent version of this License (see Section 10.2) or under the terms of a Secondary License (if permitted under the terms of Section 3.3). 2.5. Representation Each Contributor represents that the Contributor believes its Contributions are its original creation(s) or it has sufficient rights to grant the rights to its Contributions conveyed by this License. 2.6. Fair Use This License is not intended to limit any rights You have under applicable copyright doctrines of fair use, fair dealing, or other equivalents. 2.7. Conditions Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in Section 2.1. 3. Responsibilities ------------------- 3.1. Distribution of Source Form All distribution of Covered Software in Source Code Form, including any Modifications that You create or to which You contribute, must be under the terms of this License. You must inform recipients that the Source Code Form of the Covered Software is governed by the terms of this License, and how they can obtain a copy of this License. You may not attempt to alter or restrict the recipients' rights in the Source Code Form. 3.2. Distribution of Executable Form If You distribute Covered Software in Executable Form then: (a) such Covered Software must also be made available in Source Code Form, as described in Section 3.1, and You must inform recipients of the Executable Form how they can obtain a copy of such Source Code Form by reasonable means in a timely manner, at a charge no more than the cost of distribution to the recipient; and (b) You may distribute such Executable Form under the terms of this License, or sublicense it under different terms, provided that the license for the Executable Form does not attempt to limit or alter the recipients' rights in the Source Code Form under this License. 3.3. Distribution of a Larger Work You may create and distribute a Larger Work under terms of Your choice, provided that You also comply with the requirements of this License for the Covered Software. If the Larger Work is a combination of Covered Software with a work governed by one or more Secondary Licenses, and the Covered Software is not Incompatible With Secondary Licenses, this License permits You to additionally distribute such Covered Software under the terms of such Secondary License(s), so that the recipient of the Larger Work may, at their option, further distribute the Covered Software under the terms of either this License or such Secondary License(s). 3.4. Notices You may not remove or alter the substance of any license notices (including copyright notices, patent notices, disclaimers of warranty, or limitations of liability) contained within the Source Code Form of the Covered Software, except that You may alter any license notices to the extent required to remedy known factual inaccuracies. 3.5. Application of Additional Terms You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, You may do so only on Your own behalf, and not on behalf of any Contributor. You must make it absolutely clear that any such warranty, support, indemnity, or liability obligation is offered by You alone, and You hereby agree to indemnify every Contributor for any liability incurred by such Contributor as a result of warranty, support, indemnity or liability terms You offer. You may include additional disclaimers of warranty and limitations of liability specific to any jurisdiction. 4. Inability to Comply Due to Statute or Regulation --------------------------------------------------- If it is impossible for You to comply with any of the terms of this License with respect to some or all of the Covered Software due to statute, judicial order, or regulation then You must: (a) comply with the terms of this License to the maximum extent possible; and (b) describe the limitations and the code they affect. Such description must be placed in a text file included with all distributions of the Covered Software under this License. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for a recipient of ordinary skill to be able to understand it. 5. Termination -------------- 5.1. The rights granted under this License will terminate automatically if You fail to comply with any of its terms. However, if You become compliant, then the rights granted under this License from a particular Contributor are reinstated (a) provisionally, unless and until such Contributor explicitly and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor fails to notify You of the non-compliance by some reasonable means prior to 60 days after You have come back into compliance. Moreover, Your grants from a particular Contributor are reinstated on an ongoing basis if such Contributor notifies You of the non-compliance by some reasonable means, this is the first time You have received notice of non-compliance with this License from such Contributor, and You become compliant prior to 30 days after Your receipt of the notice. 5.2. If You initiate litigation against any entity by asserting a patent infringement claim (excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a Contributor Version directly or indirectly infringes any patent, then the rights granted to You by any and all Contributors for the Covered Software under Section 2.1 of this License shall terminate. 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user license agreements (excluding distributors and resellers) which have been validly granted by You or Your distributors under this License prior to termination shall survive termination. ************************************************************************ * * * 6. Disclaimer of Warranty * * ------------------------- * * * * Covered Software is provided under this License on an "as is" * * basis, without warranty of any kind, either expressed, implied, or * * statutory, including, without limitation, warranties that the * * Covered Software is free of defects, merchantable, fit for a * * particular purpose or non-infringing. The entire risk as to the * * quality and performance of the Covered Software is with You. * * Should any Covered Software prove defective in any respect, You * * (not any Contributor) assume the cost of any necessary servicing, * * repair, or correction. This disclaimer of warranty constitutes an * * essential part of this License. No use of any Covered Software is * * authorized under this License except under this disclaimer. * * * ************************************************************************ ************************************************************************ * * * 7. Limitation of Liability * * -------------------------- * * * * Under no circumstances and under no legal theory, whether tort * * (including negligence), contract, or otherwise, shall any * * Contributor, or anyone who distributes Covered Software as * * permitted above, be liable to You for any direct, indirect, * * special, incidental, or consequential damages of any character * * including, without limitation, damages for lost profits, loss of * * goodwill, work stoppage, computer failure or malfunction, or any * * and all other commercial damages or losses, even if such party * * shall have been informed of the possibility of such damages. This * * limitation of liability shall not apply to liability for death or * * personal injury resulting from such party's negligence to the * * extent applicable law prohibits such limitation. Some * * jurisdictions do not allow the exclusion or limitation of * * incidental or consequential damages, so this exclusion and * * limitation may not apply to You. * * * ************************************************************************ 8. Litigation ------------- Any litigation relating to this License may be brought only in the courts of a jurisdiction where the defendant maintains its principal place of business and such litigation shall be governed by laws of that jurisdiction, without reference to its conflict-of-law provisions. Nothing in this Section shall prevent a party's ability to bring cross-claims or counter-claims. 9. Miscellaneous ---------------- This License represents the complete agreement concerning the subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not be used to construe this License against a Contributor. 10. Versions of the License --------------------------- 10.1. New Versions Mozilla Foundation is the license steward. Except as provided in Section 10.3, no one other than the license steward has the right to modify or publish new versions of this License. Each version will be given a distinguishing version number. 10.2. Effect of New Versions You may distribute the Covered Software under the terms of the version of the License under which You originally received the Covered Software, or under the terms of any subsequent version published by the license steward. 10.3. Modified Versions If you create software not governed by this License, and you want to create a new license for such software, you may create and use a modified version of this License if you rename the license and remove any references to the name of the license steward (except to note that such modified license differs from this License). 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses If You choose to distribute Source Code Form that is Incompatible With Secondary Licenses under the terms of this version of the License, the notice described in Exhibit B of this License must be attached. Exhibit A - Source Code Form License Notice ------------------------------------------- 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/. If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. You may add additional accurate notices of copyright ownership. Exhibit B - "Incompatible With Secondary Licenses" Notice --------------------------------------------------------- This Source Code Form is "Incompatible With Secondary Licenses", as defined by the Mozilla Public License, v. 2.0. xmpp-parsers-0.22.0/README.md000064400000000000000000000005761046102023000136350ustar 00000000000000xmpp-parsers ============ What’s this? ------------ A crate which provides parsers and serialisers for most XMPP elements, to avoid having to deal with XML manually. It validates its inputs by default, and aims at providing useful errors when a particular XML element was invalid. What license is it under? ------------------------- MPL-2.0 or later, see the `LICENSE` file. xmpp-parsers-0.22.0/doap.xml000064400000000000000000001041401046102023000140130ustar 00000000000000 xmpp-parsers 2017-04-18 Collection of parsers and serialisers for XMPP extensions Collection de parseurs et de sérialiseurs pour extensions XMPP TODO TODO Rust linux windows macos android Link Mauve aaa4dac2b31c1be4ee8f8e2ab986d34fb261974f pep. 99bcf9784288e323b0d2dea9c9ac7a2ede98395a partial 2.9 0.1.0 complete 2.5rc3 0.1.0 complete 1.32.0 0.5.0 complete 2.0 0.1.0 complete 1.1 0.10.0 partial 1.2 0.21.0 Only for XEP-0048 storage partial 1.2 0.21.0 Only photo element is supported for XEP-0054 complete 1.0 0.1.0 partial 1.15.8 0.5.0 partial 1.5 0.21.0 Only the <x/> element, for compatibility with how Conversations shares files complete 1.2 0.1.0 there is no specific module for this, the feature is all in the XEP-0004 module complete 1.5.4 0.15.0 complete 2.4 0.6.0 complete 1.1 0.9.0 complete 1.1.4 0.13.0 complete 2.1 0.1.0 complete 1.1 0.8.0 complete 1.2.1 0.9.0 complete 1.6 0.10.0 complete 1.5.1 0.4.0 complete 1.2 0.15.0 complete 1.0.2 0.21.0 complete 1.1 0.21.0 complete 1.0.1 0.13.0 complete 1.1.2 0.1.0 complete 1.2.0 0.13.0 complete 1.1 0.10.0 complete 1.1 0.13.0 complete 1.1.1 0.18.0 complete 1.4.0 0.1.0 complete 1.3 0.9.0 complete 1.6 0.10.0 complete 2.0.1 0.1.0 complete 2.0 0.14.0 complete 2.0 0.1.0 complete 0.7.2 0.19.0 complete 1.0 0.1.0 complete 1.0 0.1.0 complete 1.0 0.15.0 complete 0.19.1 0.1.0 complete 0.3 0.16.0 complete 1.0.3 0.2.0 complete 1.0 0.1.0 complete 0.4.2 0.21.0 partial 0.6.3 0.14.0 only the namespace is included for now complete 0.13.0 0.15.0 partial 1.0.1 0.16.0 Only supported in payload-type, and only for rtcp-fb. partial 1.1.1 0.18.0 Parameters aren’t yet implemented. complete 1.0 0.1.0 complete 0.6.0 0.1.0 complete 1.0 0.19.1 complete 1.1.0 0.1.0 complete 1.1.1 0.1.0 complete 1.0.2 0.3.0 complete 0.3.1 0.13.0 complete 0.1 0.16.0 complete 0.1.1 0.22.0 complete 1.0.0 0.18.0 complete 0.3 0.16.0 complete 0.3.0 0.16.0 complete 0.3 0.7.0 complete 0.4.1 0.22.0 complete 0.6.0 0.1.0 complete 1.1.0 0.19.0 removed 0.14.3 0.18.0 partial 0.4.0 0.16.0 complete 0.3.1 0.22.0 complete 0.2.0 0.1.0 complete 0.3.0 0.19.1 complete 1.0.0 0.21.0 complete 1.0.1 0.22.0 complete 0.3.0 0.1.0 complete 1.1.3 0.16.0 complete 0.1.0 0.16.0 complete 0.4.2 0.22.0 complete 0.2.0 0.19.0 complete 0.1.0 0.20.0 complete 0.2.0 0.22.0 complete 0.1.1 0.21.0 complete 1.0.1 0.22.0 0.19.2 2022-12-17 0.19.1 2022-07-13 0.19.0 2022-03-07 0.18.1 2021-01-13 0.18.0 2021-01-13 0.17.0 2020-02-15 0.16.0 2019-10-15 0.15.0 2019-09-06 0.14.0 2019-07-13 0.13.1 2019-04-12 0.13.0 2019-03-20 0.12.2 2019-01-16 0.12.1 2019-01-16 0.12.0 2019-01-16 0.11.1 2018-09-20 0.11.0 2018-08-02 0.10.0 2018-07-31 0.9.0 2017-12-27 0.8.0 2017-11-30 0.7.1 2017-11-30 0.7.0 2017-11-30 0.6.0 2017-11-30 0.5.0 2017-11-30 0.4.0 2017-11-30 0.3.0 2017-11-30 0.2.0 2017-11-30 0.1.0 2017-11-30 xmpp-parsers-0.22.0/examples/generate-caps.rs000064400000000000000000000040561046102023000172550ustar 00000000000000// Copyright (c) 2019 Emmanuel Gil Peyrot // // 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 std::env; use std::io::{self, Read}; use xmpp_parsers::{ caps::{compute_disco as compute_disco_caps, hash_caps, Caps}, disco::DiscoInfoResult, ecaps2::{compute_disco as compute_disco_ecaps2, hash_ecaps2, ECaps2}, hashes::Algo, minidom::Element, Error, }; fn get_caps(disco: &DiscoInfoResult, node: String) -> Result { let data = compute_disco_caps(&disco); let hash = hash_caps(&data, Algo::Sha_1)?; Ok(Caps::new(node, hash)) } fn get_ecaps2(disco: &DiscoInfoResult) -> Result { let data = compute_disco_ecaps2(&disco)?; let hash_sha256 = hash_ecaps2(&data, Algo::Sha_256)?; let hash_sha3_256 = hash_ecaps2(&data, Algo::Sha3_256)?; let hash_blake2b_256 = hash_ecaps2(&data, Algo::Blake2b_256)?; Ok(ECaps2::new(vec![ hash_sha256, hash_sha3_256, hash_blake2b_256, ])) } fn main() -> Result<(), Box> { let args: Vec<_> = env::args().collect(); if args.len() != 2 { println!("Usage: {} ", args[0]); std::process::exit(1); } let node = args[1].clone(); eprintln!("Reading a disco#info payload from stdin..."); // Read from stdin. let stdin = io::stdin(); let mut data = String::new(); let mut handle = stdin.lock(); handle.read_to_string(&mut data)?; // Parse the payload into a DiscoInfoResult. let elem: Element = data.parse()?; let disco = DiscoInfoResult::try_from(elem)?; // Compute both kinds of caps. let caps = get_caps(&disco, node)?; let ecaps2 = get_ecaps2(&disco)?; // Print them. let caps_elem = Element::from(caps); let ecaps2_elem = Element::from(ecaps2); println!("{}", String::from(&caps_elem)); println!("{}", String::from(&ecaps2_elem)); Ok(()) } xmpp-parsers-0.22.0/parsers.diff000064400000000000000000000113351046102023000146620ustar 00000000000000diff --git a/parsers/src/util/macros.rs b/parsers/src/util/macros.rs index 3709b9da..5a9cf896 100644 --- a/parsers/src/util/macros.rs +++ b/parsers/src/util/macros.rs @@ -15,13 +15,13 @@ macro_rules! get_attr { ) }; ($elem:ident, $attr:tt, Option, $value:ident, $func:expr) => { - match $elem.attr(::xso::exports::rxml::xml_ncname!($attr).into()) { + match $elem.attr($attr) { Some($value) => Some($func), None => None, } }; ($elem:ident, $attr:tt, Required, $value:ident, $func:expr) => { - match $elem.attr(::xso::exports::rxml::xml_ncname!($attr).into()) { + match $elem.attr($attr) { Some($value) => $func, None => { return Err(xso::error::Error::Other( @@ -32,7 +32,7 @@ macro_rules! get_attr { } }; ($elem:ident, $attr:tt, Default, $value:ident, $func:expr) => { - match $elem.attr(::xso::exports::rxml::xml_ncname!($attr).into()) { + match $elem.attr($attr) { Some($value) => $func, None => ::core::default::Default::default(), } diff --git a/parsers/src/xhtml.rs b/parsers/src/xhtml.rs index 90232636..998c4b13 100644 --- a/parsers/src/xhtml.rs +++ b/parsers/src/xhtml.rs @@ -77,7 +77,7 @@ impl TryFrom for XhtmlIm { if child.is("body", ns::XHTML) { let child = child.clone(); let lang = child - .attr_ns(rxml::Namespace::xml(), rxml::xml_ncname!("lang").into()) + .attr_ns(rxml::Namespace::xml(), "lang") .unwrap_or("") .to_string(); let body = Body::try_from(child)?; @@ -168,12 +168,9 @@ impl TryFrom for Body { } Ok(Body { - style: parse_css(elem.attr(rxml::xml_ncname!("style"))), + style: parse_css(elem.attr("style")), xml_lang: elem - .attr_ns( - &Into::::into(String::from("xml")), - rxml::xml_ncname!("lang").into(), - ) + .attr_ns("xml", "lang") .map(|xml_lang| xml_lang.to_string()), children, }) @@ -329,51 +326,51 @@ impl TryFrom for Tag { Ok(match elem.name() { "a" => Tag::A { href: elem - .attr(rxml::xml_ncname!("href")) + .attr("href") .map(|href| href.to_string()), style: parse_css(elem.attr(rxml::xml_ncname!("style"))), type_: elem - .attr(rxml::xml_ncname!("type")) + .attr("type") .map(|type_| type_.to_string()), children, }, "blockquote" => Tag::Blockquote { - style: parse_css(elem.attr(rxml::xml_ncname!("style"))), + style: parse_css(elem.attr("style")), children, }, "br" => Tag::Br, "cite" => Tag::Cite { - style: parse_css(elem.attr(rxml::xml_ncname!("style"))), + style: parse_css(elem.attr("style")), children, }, "em" => Tag::Em { children }, "img" => Tag::Img { src: elem - .attr(rxml::xml_ncname!("src")) + .attr("src") .map(|src| src.to_string()), alt: elem - .attr(rxml::xml_ncname!("alt")) + .attr("alt") .map(|alt| alt.to_string()), }, "li" => Tag::Li { - style: parse_css(elem.attr(rxml::xml_ncname!("style"))), + style: parse_css(elem.attr("style")), children, }, "ol" => Tag::Ol { - style: parse_css(elem.attr(rxml::xml_ncname!("style"))), + style: parse_css(elem.attr("style")), children, }, "p" => Tag::P { - style: parse_css(elem.attr(rxml::xml_ncname!("style"))), + style: parse_css(elem.attr("style")), children, }, "span" => Tag::Span { - style: parse_css(elem.attr(rxml::xml_ncname!("style"))), + style: parse_css(elem.attr("style")), children, }, "strong" => Tag::Strong { children }, "ul" => Tag::Ul { - style: parse_css(elem.attr(rxml::xml_ncname!("style"))), + style: parse_css(elem.attr("style")), children, }, _ => Tag::Unknown(children), xmpp-parsers-0.22.0/src/attention.rs000064400000000000000000000043011046102023000155060ustar 00000000000000// Copyright (c) 2017 Emmanuel Gil Peyrot // // 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 xso::{AsXml, FromXml}; use crate::message::MessagePayload; use crate::ns; /// Requests the attention of the recipient. #[derive(FromXml, AsXml, Debug, Clone, PartialEq)] #[xml(namespace = ns::ATTENTION, name = "attention")] pub struct Attention; impl MessagePayload for Attention {} #[cfg(test)] mod tests { use super::*; use minidom::Element; #[cfg(not(feature = "disable-validation"))] use xso::error::{Error, FromElementError}; #[test] fn test_size() { assert_size!(Attention, 0); } #[test] fn test_simple() { let elem: Element = "".parse().unwrap(); Attention::try_from(elem).unwrap(); } #[cfg(not(feature = "disable-validation"))] #[test] fn test_invalid_child() { let elem: Element = "" .parse() .unwrap(); let error = Attention::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!(message, "Unknown child in Attention element."); } #[cfg(not(feature = "disable-validation"))] #[test] fn test_invalid_attribute() { let elem: Element = "" .parse() .unwrap(); let error = Attention::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!(message, "Unknown attribute in Attention element."); } #[test] fn test_serialise() { let elem: Element = "".parse().unwrap(); let attention = Attention; let elem2: Element = attention.into(); assert_eq!(elem, elem2); } } xmpp-parsers-0.22.0/src/avatar.rs000064400000000000000000000101461046102023000147630ustar 00000000000000// Copyright (c) 2019 Emmanuel Gil Peyrot // // 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 xso::{ text::{Base64, StripWhitespace, TextCodec}, AsXml, FromXml, }; use crate::hashes::Sha1HexAttribute; use crate::ns; use crate::pubsub::PubSubPayload; /// Communicates information about an avatar. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::AVATAR_METADATA, name = "metadata")] pub struct Metadata { /// List of information elements describing this avatar. #[xml(child(n = ..))] pub infos: Vec, } impl PubSubPayload for Metadata {} /// Communicates avatar metadata. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::AVATAR_METADATA, name = "info")] pub struct Info { /// The size of the image data in bytes. #[xml(attribute)] pub bytes: u32, /// The width of the image in pixels. #[xml(attribute(default))] pub width: Option, /// The height of the image in pixels. #[xml(attribute(default))] pub height: Option, /// The SHA-1 hash of the image data for the specified content-type. #[xml(attribute)] pub id: Sha1HexAttribute, /// The IANA-registered content type of the image data. #[xml(attribute = "type")] pub type_: String, /// The http: or https: URL at which the image data file is hosted. #[xml(attribute(default))] pub url: Option, } /// The actual avatar data. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::AVATAR_DATA, name = "data")] pub struct Data { /// Vector of bytes representing the avatar’s image. #[xml(text(codec = Base64.filtered(StripWhitespace)))] pub data: Vec, } impl PubSubPayload for Data {} #[cfg(test)] mod tests { use super::*; use crate::hashes::Algo; use minidom::Element; #[cfg(not(feature = "disable-validation"))] use xso::error::{Error, FromElementError}; #[cfg(target_pointer_width = "32")] #[test] fn test_size() { assert_size!(Metadata, 12); assert_size!(Info, 60); assert_size!(Data, 12); } #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(Metadata, 24); assert_size!(Info, 112); assert_size!(Data, 24); } #[test] fn test_simple() { let elem: Element = " " .parse() .unwrap(); let metadata = Metadata::try_from(elem).unwrap(); assert_eq!(metadata.infos.len(), 1); let info = &metadata.infos[0]; assert_eq!(info.bytes, 12345); assert_eq!(info.width, Some(64)); assert_eq!(info.height, Some(64)); assert_eq!(info.id.algo, Algo::Sha_1); assert_eq!(info.type_, "image/png"); assert_eq!(info.url, None); assert_eq!( info.id.hash, [ 17, 31, 75, 60, 80, 215, 176, 223, 114, 157, 41, 155, 198, 248, 233, 239, 144, 102, 151, 31 ] ); let elem: Element = "AAAA" .parse() .unwrap(); let data = Data::try_from(elem).unwrap(); assert_eq!(data.data, b"\0\0\0"); } #[cfg(not(feature = "disable-validation"))] #[test] fn test_invalid() { let elem: Element = "" .parse() .unwrap(); let error = Data::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!(message, "Unknown attribute in Data element.") } } xmpp-parsers-0.22.0/src/bind.rs000064400000000000000000000116731046102023000144270ustar 00000000000000// Copyright (c) 2018 Emmanuel Gil Peyrot // // 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 xso::{AsXml, FromXml}; use crate::iq::{IqResultPayload, IqSetPayload}; use crate::ns; use jid::{FullJid, Jid}; /// The bind feature exposed in stream:features. #[derive(FromXml, AsXml, Debug, Clone, PartialEq)] #[xml(namespace = ns::BIND, name = "bind")] pub struct BindFeature { /// Present if bind is required. #[xml(child(default))] required: Option, } /// Notes that bind is required. #[derive(FromXml, AsXml, Debug, Clone, PartialEq)] #[xml(namespace = ns::BIND, name = "required")] pub struct Required; /// The request for resource binding, which is the process by which a client /// can obtain a full JID and start exchanging on the XMPP network. /// /// See #[derive(FromXml, AsXml, Debug, Clone, PartialEq)] #[xml(namespace = ns::BIND, name = "bind")] pub struct BindQuery { /// Requests this resource, the server may associate another one though. /// /// If this is None, we request no particular resource, and a random one /// will be affected by the server. #[xml(extract(default, fields(text(type_ = String))))] resource: Option, } impl BindQuery { /// Creates a resource binding request. pub fn new(resource: Option) -> BindQuery { BindQuery { resource } } } impl IqSetPayload for BindQuery {} /// The response for resource binding, containing the client’s full JID. /// /// See #[derive(FromXml, AsXml, Debug, Clone, PartialEq)] #[xml(namespace = ns::BIND, name = "bind")] pub struct BindResponse { /// The full JID returned by the server for this client. #[xml(extract(fields(text(type_ = FullJid))))] jid: FullJid, } impl IqResultPayload for BindResponse {} impl From for FullJid { fn from(bind: BindResponse) -> FullJid { bind.jid } } impl From for Jid { fn from(bind: BindResponse) -> Jid { Jid::from(bind.jid) } } #[cfg(test)] mod tests { use super::*; use minidom::Element; #[cfg(not(feature = "disable-validation"))] use xso::error::{Error, FromElementError}; #[cfg(target_pointer_width = "32")] #[test] fn test_size() { assert_size!(BindFeature, 1); assert_size!(Required, 0); assert_size!(BindQuery, 12); assert_size!(BindResponse, 16); } #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(BindFeature, 1); assert_size!(Required, 0); assert_size!(BindQuery, 24); assert_size!(BindResponse, 32); } #[test] fn test_simple() { let elem: Element = "" .parse() .unwrap(); let bind = BindQuery::try_from(elem).unwrap(); assert_eq!(bind.resource, None); let elem: Element = "Hello™" .parse() .unwrap(); let bind = BindQuery::try_from(elem).unwrap(); // FIXME: “™” should be resourceprep’d into “TM” here… //assert_eq!(bind.resource.unwrap(), "HelloTM"); assert_eq!(bind.resource.unwrap(), "Hello™"); let elem: Element = "coucou@linkmauve.fr/Hello™" .parse() .unwrap(); let bind = BindResponse::try_from(elem).unwrap(); assert_eq!( bind.jid, FullJid::new("coucou@linkmauve.fr/HelloTM").unwrap() ); } #[cfg(not(feature = "disable-validation"))] #[test] fn test_invalid_resource() { let elem: Element = "resource" .parse() .unwrap(); let error = BindQuery::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!( message, "Unknown attribute in extraction for field 'resource' in BindQuery element." ); let elem: Element = "resource" .parse() .unwrap(); let error = BindQuery::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!( message, "Unknown child in extraction for field 'resource' in BindQuery element." ); } } xmpp-parsers-0.22.0/src/bind2.rs000064400000000000000000000111221046102023000144760ustar 00000000000000// Copyright (c) 2024 Emmanuel Gil Peyrot // // 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 xso::{AsXml, FromXml}; use crate::mam; use crate::ns; use minidom::Element; /// Represents the `` element, as sent by the server in SASL 2 to advertise which features /// can be enabled during the binding step. #[derive(FromXml, AsXml, Debug, Clone, PartialEq)] #[xml(namespace = ns::BIND2, name = "bind")] pub struct BindFeature { /// The features that can be enabled by the client. #[xml(extract(default, name = "inline", fields(extract(n = .., name = "feature", fields(attribute(name = "var", type_ = String))))))] pub inline_features: Vec, } /// Represents a `` element, as sent by the client inline in the `` SASL 2 /// element, to perform the binding at the same time as the authentication. #[derive(FromXml, AsXml, Debug, Clone, PartialEq)] #[xml(namespace = ns::BIND2, name = "bind")] pub struct BindQuery { /// Short text string that typically identifies the software the user is using, mostly useful /// for diagnostic purposes for users, operators and developers. This tag may be visible to /// other entities on the XMPP network. #[xml(extract(default, fields(text(type_ = String))))] pub tag: Option, /// Features that the client requests to be automatically enabled for its new session. #[xml(element(n = ..))] pub payloads: Vec, } /// Represents a `` element, which tells the client its resource is bound, alongside other /// requests. #[derive(FromXml, AsXml, Debug, Clone, PartialEq)] #[xml(namespace = ns::BIND2, name = "bound")] pub struct Bound { /// Indicates which messages got missed by this particular device, start is the oldest message /// and end is the newest, before this connection. #[xml(child(default))] pub mam_metadata: Option, /// Additional payloads which happened during the binding process. #[xml(element(n = ..))] pub payloads: Vec, } #[cfg(test)] mod tests { use super::*; #[cfg(target_pointer_width = "32")] #[test] fn test_size() { assert_size!(BindFeature, 12); assert_size!(BindQuery, 24); assert_size!(Bound, 68); } #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(BindFeature, 24); assert_size!(BindQuery, 48); assert_size!(Bound, 104); } #[test] fn test_empty() { let elem: Element = "".parse().unwrap(); let bind = BindQuery::try_from(elem).unwrap(); assert_eq!(bind.tag, None); assert_eq!(bind.payloads.len(), 0); } #[test] fn test_simple() { // Example 1 let elem: Element = "" .parse() .unwrap(); let bind = BindFeature::try_from(elem.clone()).unwrap(); assert_eq!(bind.inline_features.len(), 3); assert_eq!(bind.inline_features[0], "urn:xmpp:carbons:2"); assert_eq!(bind.inline_features[1], "urn:xmpp:csi:0"); assert_eq!(bind.inline_features[2], "urn:xmpp:sm:3"); let elem2 = bind.into(); assert_eq!(elem, elem2); // Example 2 let elem: Element = "AwesomeXMPP" .parse() .unwrap(); let bind = BindQuery::try_from(elem).unwrap(); assert_eq!(bind.tag.unwrap(), "AwesomeXMPP"); assert_eq!(bind.payloads.len(), 0); // Example 3 let elem: Element = "AwesomeXMPP".parse().unwrap(); let bind = BindQuery::try_from(elem).unwrap(); assert_eq!(bind.tag.unwrap(), "AwesomeXMPP"); assert_eq!(bind.payloads.len(), 3); // Example 4 let elem: Element = "".parse().unwrap(); let bound = Bound::try_from(elem).unwrap(); assert!(bound.mam_metadata.is_some()); assert_eq!(bound.payloads.len(), 0); } } xmpp-parsers-0.22.0/src/blocking.rs000064400000000000000000000150611046102023000152760ustar 00000000000000// Copyright (c) 2017 Emmanuel Gil Peyrot // // 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 xso::{AsXml, FromXml}; use crate::iq::{IqGetPayload, IqResultPayload, IqSetPayload}; use crate::ns; use jid::Jid; /// The element requesting the blocklist, the result iq will contain a /// [BlocklistResult]. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::BLOCKING, name = "blocklist")] pub struct BlocklistRequest; impl IqGetPayload for BlocklistRequest {} /// The element containing the current blocklist, as a reply from /// [BlocklistRequest]. #[derive(FromXml, AsXml, Debug, Clone, PartialEq)] #[xml(namespace = ns::BLOCKING, name = "blocklist")] pub struct BlocklistResult { /// List of JIDs affected by this command. #[xml(extract(n = .., name = "item", fields(attribute(name = "jid", type_ = Jid))))] pub items: Vec, } impl IqResultPayload for BlocklistResult {} /// A query to block one or more JIDs. // TODO: Prevent zero elements from being allowed. #[derive(FromXml, AsXml, Debug, Clone, PartialEq)] #[xml(namespace = ns::BLOCKING, name = "block")] pub struct Block { /// List of JIDs affected by this command. #[xml(extract(n = .., name = "item", fields(attribute(name = "jid", type_ = Jid))))] pub items: Vec, } impl IqSetPayload for Block {} /// A query to unblock one or more JIDs, or all of them. /// /// Warning: not putting any JID there means clearing out the blocklist. #[derive(FromXml, AsXml, Debug, Clone, PartialEq)] #[xml(namespace = ns::BLOCKING, name = "unblock")] pub struct Unblock { /// List of JIDs affected by this command. #[xml(extract(n = .., name = "item", fields(attribute(name = "jid", type_ = Jid))))] pub items: Vec, } impl IqSetPayload for Unblock {} /// The application-specific error condition when a message is blocked. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::BLOCKING_ERRORS, name = "blocked")] pub struct Blocked; #[cfg(test)] mod tests { #[cfg(not(feature = "disable-validation"))] use xso::error::{Error, FromElementError}; use super::*; use minidom::Element; #[cfg(target_pointer_width = "32")] #[test] fn test_size() { assert_size!(BlocklistRequest, 0); assert_size!(BlocklistResult, 12); assert_size!(Block, 12); assert_size!(Unblock, 12); } #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(BlocklistRequest, 0); assert_size!(BlocklistResult, 24); assert_size!(Block, 24); assert_size!(Unblock, 24); } #[test] fn test_simple() { let elem: Element = "".parse().unwrap(); let request_elem = elem.clone(); BlocklistRequest::try_from(request_elem).unwrap(); let result_elem = elem.clone(); let result = BlocklistResult::try_from(result_elem).unwrap(); assert!(result.items.is_empty()); let elem: Element = "".parse().unwrap(); let block = Block::try_from(elem).unwrap(); assert!(block.items.is_empty()); let elem: Element = "".parse().unwrap(); let unblock = Unblock::try_from(elem).unwrap(); assert!(unblock.items.is_empty()); } #[test] fn test_items() { let elem: Element = "".parse().unwrap(); let two_items = vec![ Jid::new("coucou@coucou").unwrap(), Jid::new("domain").unwrap(), ]; let result_elem = elem.clone(); let result = BlocklistResult::try_from(result_elem).unwrap(); assert_eq!(result.items, two_items); let elem: Element = "".parse().unwrap(); let block = Block::try_from(elem).unwrap(); assert_eq!(block.items, two_items); let elem: Element = "".parse().unwrap(); let unblock = Unblock::try_from(elem).unwrap(); assert_eq!(unblock.items, two_items); } #[cfg(not(feature = "disable-validation"))] #[test] fn test_invalid() { let elem: Element = "" .parse() .unwrap(); let request_elem = elem.clone(); let error = BlocklistRequest::try_from(request_elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!(message, "Unknown attribute in BlocklistRequest element."); let result_elem = elem.clone(); let error = BlocklistResult::try_from(result_elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!(message, "Unknown attribute in BlocklistResult element."); let elem: Element = "" .parse() .unwrap(); let error = Block::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!(message, "Unknown attribute in Block element."); let elem: Element = "" .parse() .unwrap(); let error = Unblock::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!(message, "Unknown attribute in Unblock element."); } #[cfg(not(feature = "disable-validation"))] #[test] fn test_non_empty_blocklist_request() { let elem: Element = "".parse().unwrap(); let error = BlocklistRequest::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!(message, "Unknown child in BlocklistRequest element."); } } xmpp-parsers-0.22.0/src/bob.rs000064400000000000000000000147411046102023000142540ustar 00000000000000// Copyright (c) 2019 Emmanuel Gil Peyrot // // 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::str::FromStr; use xso::{error::Error, text::Base64, AsXml, AsXmlText, FromXml, FromXmlText}; use crate::hashes::{Algo, Hash}; use crate::ns; use minidom::IntoAttributeValue; /// A Content-ID, as defined in RFC2111. /// /// The text value SHOULD be of the form algo+hash@bob.xmpp.org, this struct /// enforces that format. #[derive(Clone, Debug, PartialEq)] pub struct ContentId { hash: Hash, } impl FromStr for ContentId { type Err = Error; fn from_str(s: &str) -> Result { let temp: Vec<_> = s.splitn(2, '@').collect(); let temp: Vec<_> = match temp[..] { [lhs, rhs] => { if rhs != "bob.xmpp.org" { return Err(Error::Other("Wrong domain for cid URI.")); } lhs.splitn(2, '+').collect() } _ => return Err(Error::Other("Missing @ in cid URI.")), }; let (algo, hex) = match temp[..] { [lhs, rhs] => { let algo = match lhs { "sha1" => Algo::Sha_1, "sha256" => Algo::Sha_256, _ => unimplemented!(), }; (algo, rhs) } _ => return Err(Error::Other("Missing + in cid URI.")), }; let hash = Hash::from_hex(algo, hex).map_err(Error::text_parse_error)?; Ok(ContentId { hash }) } } impl FromXmlText for ContentId { fn from_xml_text(value: String) -> Result { value.parse().map_err(Error::text_parse_error) } } impl AsXmlText for ContentId { fn as_xml_text(&self) -> Result, Error> { let algo = match self.hash.algo { Algo::Sha_1 => "sha1", Algo::Sha_256 => "sha256", _ => unimplemented!(), }; Ok(Cow::Owned(format!( "{}+{}@bob.xmpp.org", algo, self.hash.to_hex() ))) } } impl IntoAttributeValue for ContentId { fn into_attribute_value(self) -> Option { let algo = match self.hash.algo { Algo::Sha_1 => "sha1", Algo::Sha_256 => "sha256", _ => unimplemented!(), }; Some(format!("{}+{}@bob.xmpp.org", algo, self.hash.to_hex())) } } /// Request for an uncached cid file. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::BOB, name = "data")] pub struct Data { /// The cid in question. #[xml(attribute)] pub cid: ContentId, /// How long to cache it (in seconds). #[xml(attribute(default, name = "max-age"))] pub max_age: Option, /// The MIME type of the data being transmitted. /// /// See the [IANA MIME Media Types Registry][1] for a list of /// registered types, but unregistered or yet-to-be-registered are /// accepted too. /// /// [1]: #[xml(attribute(default, name = "type"))] pub type_: Option, /// The actual data. #[xml(text = Base64)] pub data: Vec, } #[cfg(test)] mod tests { use super::*; use minidom::Element; use xso::error::FromElementError; #[cfg(target_pointer_width = "32")] #[test] fn test_size() { assert_size!(ContentId, 24); assert_size!(Data, 56); } #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(ContentId, 48); assert_size!(Data, 112); } #[test] fn test_simple() { let cid: ContentId = "sha1+8f35fef110ffc5df08d579a50083ff9308fb6242@bob.xmpp.org" .parse() .unwrap(); assert_eq!(cid.hash.algo, Algo::Sha_1); assert_eq!( cid.hash.hash, b"\x8f\x35\xfe\xf1\x10\xff\xc5\xdf\x08\xd5\x79\xa5\x00\x83\xff\x93\x08\xfb\x62\x42" ); assert_eq!( cid.into_attribute_value().unwrap(), "sha1+8f35fef110ffc5df08d579a50083ff9308fb6242@bob.xmpp.org" ); let elem: Element = "".parse().unwrap(); let data = Data::try_from(elem).unwrap(); assert_eq!(data.cid.hash.algo, Algo::Sha_1); assert_eq!( data.cid.hash.hash, b"\x8f\x35\xfe\xf1\x10\xff\xc5\xdf\x08\xd5\x79\xa5\x00\x83\xff\x93\x08\xfb\x62\x42" ); assert!(data.max_age.is_none()); assert!(data.type_.is_none()); assert!(data.data.is_empty()); } #[test] fn invalid_cid() { let error = "Hello world!".parse::().unwrap_err(); let message = match error { Error::Other(string) => string, _ => panic!(), }; assert_eq!(message, "Missing @ in cid URI."); let error = "Hello world@bob.xmpp.org".parse::().unwrap_err(); let message = match error { Error::Other(string) => string, _ => panic!(), }; assert_eq!(message, "Missing + in cid URI."); let error = "sha1+1234@coucou.linkmauve.fr" .parse::() .unwrap_err(); let message = match error { Error::Other(string) => string, _ => panic!(), }; assert_eq!(message, "Wrong domain for cid URI."); let error = "sha1+invalid@bob.xmpp.org" .parse::() .unwrap_err(); let message = match error { Error::TextParseError(error) if error.is::() => error, _ => panic!(), }; assert_eq!(message.to_string(), "invalid digit found in string"); } #[test] #[cfg_attr(feature = "disable-validation", should_panic = "Result::unwrap_err")] fn unknown_child() { let elem: Element = "" .parse() .unwrap(); let error = Data::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!(message, "Unknown child in Data element."); } } xmpp-parsers-0.22.0/src/bookmarks.rs000064400000000000000000000141501046102023000154740ustar 00000000000000// Copyright (c) 2018 Emmanuel Gil Peyrot // // 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/. //! //! Chatroom bookmarks from [XEP-0048](https://xmpp.org/extensions/attic/xep-0048-1.0.html). You should never use this, but use //! [`bookmarks2`][`crate::bookmarks2`], or [`private::Query`][`crate::private::Query`] for legacy servers which do not advertise //! `urn:xmpp:bookmarks:1#compat` on the user's BareJID in a disco info request. //! //! See [ModernXMPP docs](https://docs.modernxmpp.org/client/groupchat/#bookmarks) on how to handle all historic //! and newer specifications for your clients. //! //! The [`Conference`][crate::bookmarks::Conference] struct used in [`private::Query`][`crate::private::Query`] is the one from this module. Only the querying mechanism changes from a legacy PubSub implementation here, to a legacy Private XML Query implementation in that other module. The [`Conference`][crate::bookmarks2::Conference] element from the [`bookmarks2`][crate::bookmarks2] module is a different structure, but conversion is possible from [`bookmarks::Conference`][crate::bookmarks::Conference] to [`bookmarks2::Conference`][crate::bookmarks2::Conference] via the [`Conference::into_bookmarks2`][crate::bookmarks::Conference::into_bookmarks2] method. use xso::{AsXml, FromXml}; use jid::BareJid; pub use crate::bookmarks2; use crate::jid::ResourcePart; use crate::ns; /// A conference bookmark. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::BOOKMARKS, name = "conference")] pub struct Conference { /// Whether a conference bookmark should be joined automatically. #[xml(attribute(default))] pub autojoin: bool, /// The JID of the conference. #[xml(attribute)] pub jid: BareJid, /// A user-defined name for this conference. #[xml(attribute(default))] pub name: Option, /// The nick the user will use to join this conference. #[xml(extract(default, fields(text(type_ = ResourcePart))))] pub nick: Option, /// The password required to join this conference. #[xml(extract(default, fields(text(type_ = String))))] pub password: Option, } impl Conference { /// Turns a XEP-0048 Conference element into a XEP-0402 "Bookmarks2" Conference element, in a /// tuple with the room JID. pub fn into_bookmarks2(self) -> (BareJid, bookmarks2::Conference) { ( self.jid, bookmarks2::Conference { autojoin: self.autojoin, name: self.name, nick: self.nick, password: self.password, extensions: None, }, ) } } /// An URL bookmark. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::BOOKMARKS, name = "url")] pub struct Url { /// A user-defined name for this URL. #[xml(attribute(default))] pub name: Option, /// The URL of this bookmark. #[xml(attribute)] pub url: String, } /// Container element for multiple bookmarks. #[derive(FromXml, AsXml, PartialEq, Debug, Clone, Default)] #[xml(namespace = ns::BOOKMARKS, name = "storage")] pub struct Storage { /// Conferences the user has expressed an interest in. #[xml(child(n = ..))] pub conferences: Vec, /// URLs the user is interested in. #[xml(child(n = ..))] pub urls: Vec, } impl Storage { /// Create an empty bookmarks storage. pub fn new() -> Storage { Storage::default() } } #[cfg(test)] mod tests { use super::*; use minidom::Element; #[cfg(target_pointer_width = "32")] #[test] fn test_size() { assert_size!(Conference, 56); assert_size!(Url, 24); assert_size!(Storage, 24); } #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(Conference, 112); assert_size!(Url, 48); assert_size!(Storage, 48); } #[test] fn empty() { let elem: Element = "".parse().unwrap(); let elem1 = elem.clone(); let storage = Storage::try_from(elem).unwrap(); assert_eq!(storage.conferences.len(), 0); assert_eq!(storage.urls.len(), 0); let elem2 = Element::from(Storage::new()); assert_eq!(elem1, elem2); } #[test] fn wrong_resource() { // This emoji is not valid according to Resource prep let elem: Element = "Whatever\u{1F469}\u{1F3FE}\u{200D}\u{2764}\u{FE0F}\u{200D}\u{1F469}\u{1F3FC}".parse().unwrap(); let res = Storage::try_from(elem); assert!(res.is_err()); assert_eq!( res.unwrap_err().to_string().as_str(), "text parse error: resource doesn’t pass resourceprep validation" ); } #[test] fn complete() { let elem: Element = "Coucousecret".parse().unwrap(); let storage = Storage::try_from(elem).unwrap(); assert_eq!(storage.urls.len(), 1); assert_eq!(storage.urls[0].clone().name.unwrap(), "Example"); assert_eq!(storage.urls[0].url, "https://example.org/"); assert_eq!(storage.conferences.len(), 1); assert_eq!(storage.conferences[0].autojoin, true); assert_eq!( storage.conferences[0].jid, BareJid::new("test-muc@muc.localhost").unwrap() ); assert_eq!(storage.conferences[0].clone().name.unwrap(), "Test MUC"); assert_eq!( storage.conferences[0].clone().nick.unwrap().as_str(), "Coucou" ); assert_eq!(storage.conferences[0].clone().password.unwrap(), "secret"); } } xmpp-parsers-0.22.0/src/bookmarks2.rs000064400000000000000000000143421046102023000155610ustar 00000000000000// Copyright (c) 2019 Emmanuel Gil Peyrot // // 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/. //! //! Chatroom bookmarks from [XEP-0402](https://xmpp.org/extensions/xep-0402.html) for newer servers //! which advertise `urn:xmpp:bookmarks:1#compat` on the user's BareJID in a disco info request. //! On legacy non-compliant servers, use the [`private`][crate::private] module instead. //! //! See [ModernXMPP docs](https://docs.modernxmpp.org/client/groupchat/#bookmarks) on how to handle all historic //! and newer specifications for your clients. use xso::{AsXml, FromXml}; use crate::jid::ResourcePart; use crate::ns; use minidom::Element; /// Potential extensions in a conference. #[derive(FromXml, AsXml, Debug, Clone, Default)] #[xml(namespace = ns::BOOKMARKS2, name = "extensions")] pub struct Extensions { /// Extension elements. #[xml(element(n = ..))] pub payloads: Vec, } /// A conference bookmark. #[derive(FromXml, AsXml, Debug, Clone, Default)] #[xml(namespace = ns::BOOKMARKS2, name = "conference")] pub struct Conference { /// Whether a conference bookmark should be joined automatically. #[xml(attribute(default))] pub autojoin: bool, /// A user-defined name for this conference. #[xml(attribute(default))] pub name: Option, /// The nick the user will use to join this conference. #[xml(extract(default, fields(text(type_ = ResourcePart))))] pub nick: Option, /// The password required to join this conference. #[xml(extract(default, fields(text(type_ = String))))] pub password: Option, /// Extension elements. #[xml(child(default))] pub extensions: Option, } impl Conference { /// Create a new conference. pub fn new() -> Conference { Conference::default() } } #[cfg(test)] mod tests { use super::*; use crate::pubsub::{self, pubsub::Item as PubSubItem}; #[cfg(target_pointer_width = "32")] #[test] fn test_size() { assert_size!(Conference, 52); } #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(Conference, 104); } #[test] fn simple() { let elem: Element = "" .parse() .unwrap(); let elem1 = elem.clone(); let conference = Conference::try_from(elem).unwrap(); assert_eq!(conference.autojoin, false); assert_eq!(conference.name, None); assert_eq!(conference.nick, None); assert_eq!(conference.password, None); let elem2 = Element::from(Conference::new()); assert_eq!(elem1, elem2); } #[test] fn wrong_resource() { // This emoji is not valid according to Resource prep let elem: Element = "Whatever\u{1F469}\u{1F3FE}\u{200D}\u{2764}\u{FE0F}\u{200D}\u{1F469}\u{1F3FC}".parse().unwrap(); let res = Conference::try_from(elem); assert!(res.is_err()); assert_eq!( res.unwrap_err().to_string().as_str(), "text parse error: resource doesn’t pass resourceprep validation" ); } #[test] fn complete() { let elem: Element = "Coucousecret".parse().unwrap(); let conference = Conference::try_from(elem).unwrap(); assert_eq!(conference.autojoin, true); assert_eq!(conference.name, Some(String::from("Test MUC"))); assert_eq!(conference.clone().nick.unwrap().as_str(), "Coucou"); assert_eq!(conference.clone().password.unwrap(), "secret"); let payloads = conference.clone().extensions.unwrap().payloads; assert_eq!(payloads.len(), 1); assert!(payloads[0].is("test", "urn:xmpp:unknown")); } #[test] fn wrapped() { let elem: Element = "Coucousecret".parse().unwrap(); let item = PubSubItem::try_from(elem).unwrap(); let payload = item.payload.clone().unwrap(); println!("FOO: payload: {:?}", payload); // let conference = Conference::try_from(payload).unwrap(); let conference = Conference::try_from(payload).unwrap(); println!("FOO: conference: {:?}", conference); assert_eq!(conference.autojoin, true); assert_eq!(conference.name, Some(String::from("Test MUC"))); assert_eq!(conference.clone().nick.unwrap().as_str(), "Coucou"); assert_eq!(conference.clone().password.unwrap(), "secret"); let elem: Element = "Coucousecret".parse().unwrap(); let event = pubsub::Event::try_from(elem).unwrap(); let mut items = match event.payload { pubsub::event::Payload::Items { node, published, retracted, } => { assert_eq!(&node.0, ns::BOOKMARKS2); assert_eq!(retracted.len(), 0); published } _ => panic!(), }; assert_eq!(items.len(), 1); let item = items.pop().unwrap(); let payload = item.payload.clone().unwrap(); let conference = Conference::try_from(payload).unwrap(); assert_eq!(conference.autojoin, true); assert_eq!(conference.name, Some(String::from("Test MUC"))); assert_eq!(conference.clone().nick.unwrap().as_str(), "Coucou"); assert_eq!(conference.clone().password.unwrap(), "secret"); } } xmpp-parsers-0.22.0/src/caps.rs000064400000000000000000000253751046102023000144450ustar 00000000000000// Copyright (c) 2017 Emmanuel Gil Peyrot // // 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 xso::{AsXml, FromXml}; use crate::data_forms::DataForm; use crate::disco::{DiscoInfoQuery, DiscoInfoResult, Feature, Identity}; use crate::hashes::{Algo, Hash}; use crate::ns; use crate::presence::PresencePayload; use base64::{engine::general_purpose::STANDARD as Base64, Engine}; use blake2::Blake2bVar; use digest::{Digest, Update, VariableOutput}; use sha1::Sha1; use sha2::{Sha256, Sha512}; use sha3::{Sha3_256, Sha3_512}; /// Represents a capability hash for a given client. /// /// Warning: This protocol is insecure, you may want to switch to /// [ecaps2](../ecaps2/index.html) instead, see [this /// email](https://mail.jabber.org/pipermail/security/2009-July/000812.html). #[derive(FromXml, AsXml, Debug, Clone)] #[xml(namespace = ns::CAPS, name = "c")] pub struct Caps { /// Deprecated list of additional feature bundles. #[xml(attribute(default))] pub ext: Option, /// A URI identifying an XMPP application. #[xml(attribute)] pub node: String, /// The algorithm of the hash of these caps. #[xml(attribute)] pub hash: Algo, /// The hash of that application’s /// [disco#info](../disco/struct.DiscoInfoResult.html). #[xml(attribute(codec = Base64))] pub ver: Vec, } impl PresencePayload for Caps {} impl Caps { /// Create a Caps element from its node and hash. pub fn new>(node: N, hash: Hash) -> Caps { Caps { ext: None, node: node.into(), hash: hash.algo, ver: hash.hash, } } } fn compute_item(field: &str) -> Vec { let mut bytes = field.as_bytes().to_vec(); bytes.push(b'<'); bytes } fn compute_items Vec>(things: &[T], encode: F) -> Vec { let mut string: Vec = vec![]; let mut accumulator: Vec> = vec![]; for thing in things { let bytes = encode(thing); accumulator.push(bytes); } // This works using the expected i;octet collation. accumulator.sort(); for mut bytes in accumulator { string.append(&mut bytes); } string } fn compute_features(features: &[Feature]) -> Vec { compute_items(features, |feature| compute_item(&feature.var)) } fn compute_identities(identities: &[Identity]) -> Vec { compute_items(identities, |identity| { let lang = identity.lang.as_deref().unwrap_or_default(); let name = identity.name.as_deref().unwrap_or_default(); let string = format!("{}/{}/{}/{}", identity.category, identity.type_, lang, name); let mut vec = string.as_bytes().to_vec(); vec.push(b'<'); vec }) } fn compute_extensions(extensions: &[DataForm]) -> Vec { compute_items(extensions, |extension| { // TODO: maybe handle the error case? let mut bytes = if let Some(form_type) = extension.form_type() { form_type.as_bytes().to_vec() } else { vec![] }; bytes.push(b'<'); for field in &extension.fields { if field.var.as_deref() == Some("FORM_TYPE") { continue; } if let Some(var) = &field.var { bytes.append(&mut compute_item(var)); } bytes.append(&mut compute_items(&field.values, |value| { compute_item(value) })); } bytes }) } /// Applies the caps algorithm on the provided disco#info result, to generate /// the hash input. /// /// Warning: This protocol is insecure, you may want to switch to /// [ecaps2](../ecaps2/index.html) instead, see [this /// email](https://mail.jabber.org/pipermail/security/2009-July/000812.html). pub fn compute_disco(disco: &DiscoInfoResult) -> Vec { let identities_string = compute_identities(&disco.identities); let features_string = compute_features(&disco.features); let extensions_string = compute_extensions(&disco.extensions); let mut final_string = vec![]; final_string.extend(identities_string); final_string.extend(features_string); final_string.extend(extensions_string); final_string } /// Hashes the result of [compute_disco()] with one of the supported [hash /// algorithms](../hashes/enum.Algo.html). pub fn hash_caps(data: &[u8], algo: Algo) -> Result { Ok(Hash { hash: match algo { Algo::Sha_1 => { let hash = Sha1::digest(data); hash.to_vec() } Algo::Sha_256 => { let hash = Sha256::digest(data); hash.to_vec() } Algo::Sha_512 => { let hash = Sha512::digest(data); hash.to_vec() } Algo::Sha3_256 => { let hash = Sha3_256::digest(data); hash.to_vec() } Algo::Sha3_512 => { let hash = Sha3_512::digest(data); hash.to_vec() } Algo::Blake2b_256 => { let mut hasher = Blake2bVar::new(32).unwrap(); hasher.update(data); let mut vec = vec![0u8; 32]; hasher.finalize_variable(&mut vec).unwrap(); vec } Algo::Blake2b_512 => { let mut hasher = Blake2bVar::new(64).unwrap(); hasher.update(data); let mut vec = vec![0u8; 64]; hasher.finalize_variable(&mut vec).unwrap(); vec } Algo::Unknown(algo) => return Err(format!("Unknown algorithm: {}.", algo)), }, algo, }) } /// Helper function to create the query for the disco#info corresponding to a /// caps hash. pub fn query_caps(caps: Caps) -> DiscoInfoQuery { DiscoInfoQuery { node: Some(format!("{}#{}", caps.node, Base64.encode(&caps.ver))), } } #[cfg(test)] mod tests { use super::*; use crate::caps; use minidom::Element; #[cfg(not(feature = "disable-validation"))] use xso::error::{Error, FromElementError}; #[cfg(target_pointer_width = "32")] #[test] fn test_size() { assert_size!(Caps, 48); } #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(Caps, 96); } #[test] fn test_parse() { let elem: Element = "".parse().unwrap(); let caps = Caps::try_from(elem).unwrap(); assert_eq!(caps.node, String::from("coucou")); assert_eq!(caps.hash, Algo::Sha_256); assert_eq!( caps.ver, Base64 .decode("K1Njy3HZBThlo4moOD5gBGhn0U0oK7/CbfLlIUDi6o4=") .unwrap() ); } #[cfg(not(feature = "disable-validation"))] #[test] fn test_invalid_child() { let elem: Element = "K1Njy3HZBThlo4moOD5gBGhn0U0oK7/CbfLlIUDi6o4=".parse().unwrap(); let error = Caps::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!(message, "Unknown child in Caps element."); } #[test] fn test_simple() { let elem: Element = "".parse().unwrap(); let disco = DiscoInfoResult::try_from(elem).unwrap(); let caps = caps::compute_disco(&disco); assert_eq!(caps.len(), 50); } #[test] fn test_xep_5_2() { let elem: Element = r#" "# .parse() .unwrap(); let expected = b"client/pc//Exodus 0.9.1 urn:xmpp:dataforms:softwareinfo ipv4 ipv6 Mac 10.5.1 Psi 0.11 "# .parse() .unwrap(); let expected = b"client/pc/el/\xce\xa8 0.11 // // 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 xso::{text::Base64, AsXml, FromXml}; use crate::iq::{IqGetPayload, IqResultPayload, IqSetPayload}; use crate::ns; generate_elem_id!( /// The name of a certificate. Name, "name", SASL_CERT ); /// An X.509 certificate. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::SASL_CERT, name = "x509cert")] pub struct Cert { /// The BER X.509 data. #[xml(text = Base64)] pub data: Vec, } /// For the client to upload an X.509 certificate. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::SASL_CERT, name = "append")] pub struct Append { /// The name of this certificate. #[xml(child)] pub name: Name, /// The X.509 certificate to set. #[xml(child)] pub cert: Cert, /// This client is forbidden from managing certificates. #[xml(flag(name = "no-cert-management"))] pub no_cert_management: bool, } impl IqSetPayload for Append {} /// Client requests the current list of X.509 certificates. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::SASL_CERT, name = "items")] pub struct ListCertsQuery; impl IqGetPayload for ListCertsQuery {} generate_elem_id!( /// One resource currently using a certificate. Resource, "resource", SASL_CERT ); /// A list of resources currently using this certificate. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::SASL_CERT, name = "users")] pub struct Users { /// Resources currently using this certificate. #[xml(child(n = ..))] pub resources: Vec, } /// An X.509 certificate being set for this user. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::SASL_CERT, name = "item")] pub struct Item { /// The name of this certificate. #[xml(child)] pub name: Name, /// The X.509 certificate to set. #[xml(child)] pub cert: Cert, /// This client is forbidden from managing certificates. #[xml(flag(name = "no-cert-management"))] pub no_cert_management: bool, /// List of resources currently using this certificate. #[xml(child(default))] pub users: Option, } /// Server answers with the current list of X.509 certificates. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::SASL_CERT, name = "items")] pub struct ListCertsResponse { /// List of certificates. #[xml(child(n = ..))] pub items: Vec, } impl IqResultPayload for ListCertsResponse {} /// Client disables an X.509 certificate. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::SASL_CERT, name = "disable")] pub struct Disable { /// Name of the certificate to disable. #[xml(child)] pub name: Name, } impl IqSetPayload for Disable {} /// Client revokes an X.509 certificate. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::SASL_CERT, name = "revoke")] pub struct Revoke { /// Name of the certificate to revoke. #[xml(child)] pub name: Name, } impl IqSetPayload for Revoke {} #[cfg(test)] mod tests { use super::*; use crate::ns; use core::str::FromStr; use minidom::Element; #[cfg(target_pointer_width = "32")] #[test] fn test_size() { assert_size!(Append, 28); assert_size!(Disable, 12); assert_size!(Revoke, 12); assert_size!(ListCertsQuery, 0); assert_size!(ListCertsResponse, 12); assert_size!(Item, 40); assert_size!(Resource, 12); assert_size!(Users, 12); assert_size!(Cert, 12); } #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(Append, 56); assert_size!(Disable, 24); assert_size!(Revoke, 24); assert_size!(ListCertsQuery, 0); assert_size!(ListCertsResponse, 24); assert_size!(Item, 80); assert_size!(Resource, 24); assert_size!(Users, 24); assert_size!(Cert, 24); } #[test] fn simple() { let elem: Element = "Mobile ClientAAAA".parse().unwrap(); let append = Append::try_from(elem).unwrap(); assert_eq!(append.name.0, "Mobile Client"); assert_eq!(append.cert.data, b"\0\0\0"); let elem: Element = "Mobile Client" .parse() .unwrap(); let disable = Disable::try_from(elem).unwrap(); assert_eq!(disable.name.0, "Mobile Client"); let elem: Element = "Mobile Client" .parse() .unwrap(); let revoke = Revoke::try_from(elem).unwrap(); assert_eq!(revoke.name.0, "Mobile Client"); } #[test] fn list() { let elem: Element = r#" Mobile Client AAAA Phone Laptop BBBB "# .parse() .unwrap(); let mut list = ListCertsResponse::try_from(elem).unwrap(); assert_eq!(list.items.len(), 2); let item = list.items.pop().unwrap(); assert_eq!(item.name.0, "Laptop"); assert_eq!(item.cert.data, [4, 16, 65]); assert!(item.users.is_none()); let item = list.items.pop().unwrap(); assert_eq!(item.name.0, "Mobile Client"); assert_eq!(item.cert.data, b"\0\0\0"); assert_eq!(item.users.unwrap().resources.len(), 1); } #[test] fn test_serialise() { let append = Append { name: Name::from_str("Mobile Client").unwrap(), cert: Cert { data: b"\0\0\0".to_vec(), }, no_cert_management: false, }; let elem: Element = append.into(); assert!(elem.is("append", ns::SASL_CERT)); let disable = Disable { name: Name::from_str("Mobile Client").unwrap(), }; let elem: Element = disable.into(); assert!(elem.is("disable", ns::SASL_CERT)); let elem = elem.children().cloned().collect::>().pop().unwrap(); assert!(elem.is("name", ns::SASL_CERT)); assert_eq!(elem.text(), "Mobile Client"); } #[test] fn test_serialize_item() { let reference: Element = "Mobile ClientAAAA" .parse() .unwrap(); let item = Item { name: Name::from_str("Mobile Client").unwrap(), cert: Cert { data: b"\0\0\0".to_vec(), }, no_cert_management: false, users: None, }; let serialized: Element = item.into(); assert_eq!(serialized, reference); } #[test] fn test_serialize_append() { let reference: Element = "Mobile ClientAAAA" .parse() .unwrap(); let append = Append { name: Name::from_str("Mobile Client").unwrap(), cert: Cert { data: b"\0\0\0".to_vec(), }, no_cert_management: false, }; let serialized: Element = append.into(); assert_eq!(serialized, reference); } #[test] fn test_serialize_disable() { let reference: Element = "Mobile Client" .parse() .unwrap(); let disable = Disable { name: Name::from_str("Mobile Client").unwrap(), }; let serialized: Element = disable.into(); assert_eq!(serialized, reference); } #[test] fn test_serialize_revoke() { let reference: Element = "Mobile Client" .parse() .unwrap(); let revoke = Revoke { name: Name::from_str("Mobile Client").unwrap(), }; let serialized: Element = revoke.into(); assert_eq!(serialized, reference); } } xmpp-parsers-0.22.0/src/chatstates.rs000064400000000000000000000063071046102023000156540ustar 00000000000000// Copyright (c) 2017 Emmanuel Gil Peyrot // // 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 xso::{AsXml, FromXml}; use crate::message::MessagePayload; use crate::ns; /// Enum representing chatstate elements part of the /// `http://jabber.org/protocol/chatstates` namespace. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::CHATSTATES, exhaustive)] pub enum ChatState { /// `` #[xml(name = "active")] Active, /// `` #[xml(name = "composing")] Composing, /// `` #[xml(name = "gone")] Gone, /// `` #[xml(name = "inactive")] Inactive, /// `` #[xml(name = "paused")] Paused, } impl MessagePayload for ChatState {} #[cfg(test)] mod tests { use super::*; use crate::ns; use minidom::Element; use xso::error::{Error, FromElementError}; #[test] fn test_size() { assert_size!(ChatState, 1); } #[test] fn test_simple() { let elem: Element = "" .parse() .unwrap(); ChatState::try_from(elem).unwrap(); } #[test] fn test_invalid() { let elem: Element = "" .parse() .unwrap(); let error = ChatState::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!(message, "This is not a ChatState element."); } #[cfg(not(feature = "disable-validation"))] #[test] fn test_invalid_child() { let elem: Element = "" .parse() .unwrap(); let error = ChatState::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!(message, "Unknown child in ChatState::Gone element."); } #[cfg(not(feature = "disable-validation"))] #[test] fn test_invalid_attribute() { let elem: Element = "" .parse() .unwrap(); let error = ChatState::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!(message, "Unknown attribute in ChatState::Inactive element."); } #[test] fn test_serialise() { let chatstate = ChatState::Active; let elem: Element = chatstate.into(); assert!(elem.is("active", ns::CHATSTATES)); } } xmpp-parsers-0.22.0/src/component.rs000064400000000000000000000054251046102023000155130ustar 00000000000000// Copyright (c) 2018 Emmanuel Gil Peyrot // // 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 xso::{text::FixedHex, AsXml, FromXml}; use crate::ns; use digest::Digest; use sha1::Sha1; /// The main authentication mechanism for components. #[derive(FromXml, AsXml, PartialEq, Debug, Clone, Default)] #[xml(namespace = ns::COMPONENT, name = "handshake")] pub struct Handshake { /// If Some, contains the hex-encoded SHA-1 of the concatenation of the /// stream id and the password, and is used to authenticate against the /// server. /// /// If None, it is the successful reply from the server, the stream is now /// fully established and both sides can now exchange stanzas. #[xml(text(codec = FixedHex<20>))] pub data: Option<[u8; 20]>, } impl Handshake { /// Creates a successful reply from a server. pub fn new() -> Handshake { Handshake::default() } /// Creates an authentication request from the component. pub fn from_stream_id_and_password(stream_id: String, password: &str) -> Handshake { let input = stream_id + password; let hash = Sha1::digest(input.as_bytes()); Handshake { data: Some(hash.into()), } } } #[cfg(test)] mod tests { use super::*; use minidom::Element; #[test] fn test_size() { assert_size!(Handshake, 21); } #[test] fn test_simple() { let elem: Element = "" .parse() .unwrap(); let handshake = Handshake::try_from(elem).unwrap(); assert_eq!(handshake.data, None); let elem: Element = "9accec263ab84a43c6037ccf7cd48cb1d3f6df8e" .parse() .unwrap(); let handshake = Handshake::try_from(elem).unwrap(); assert_eq!( handshake.data, Some([ 0x9a, 0xcc, 0xec, 0x26, 0x3a, 0xb8, 0x4a, 0x43, 0xc6, 0x03, 0x7c, 0xcf, 0x7c, 0xd4, 0x8c, 0xb1, 0xd3, 0xf6, 0xdf, 0x8e ]) ); } #[test] fn test_constructors() { let handshake = Handshake::new(); assert_eq!(handshake.data, None); let stream_id = String::from("sid"); let password = "123456"; let handshake = Handshake::from_stream_id_and_password(stream_id, password); assert_eq!( handshake.data, Some([ 0x9a, 0xcc, 0xec, 0x26, 0x3a, 0xb8, 0x4a, 0x43, 0xc6, 0x03, 0x7c, 0xcf, 0x7c, 0xd4, 0x8c, 0xb1, 0xd3, 0xf6, 0xdf, 0x8e ]) ); } } xmpp-parsers-0.22.0/src/confirm.rs000064400000000000000000000050201046102023000151350ustar 00000000000000// Copyright (c) 2025 Adrien Destugues // // 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 xso::{AsXml, FromXml}; use crate::iq::IqGetPayload; use crate::message::MessagePayload; use crate::ns; /// XEP-0070 "confirm" element used to request user confirmation before accessing a protected /// HTTP resource, or as a reply to such request. /// /// This can be used as: /// - A message payload (when the request is sent to a bare JID or when receiving the reply) /// - A Set Iq payload (when the request is sent to a full JID as an Iq). The confirm element may /// also be present in responses, but should be ignored (the IQ id is used to identify the /// request in that case). #[derive(FromXml, AsXml, PartialEq, Eq, Hash, Debug, Clone)] #[xml(namespace = ns::HTTP_AUTH, name = "confirm")] pub struct Confirm { /// HTTP method used to access the resource #[xml(attribute)] pub method: String, /// URL being accessed and guarded by the authentication request #[xml(attribute)] pub url: String, /// Identifier of the authentication request #[xml(attribute)] pub id: String, } impl IqGetPayload for Confirm {} impl MessagePayload for Confirm {} #[cfg(test)] mod tests { use super::*; use crate::ns; use minidom::Element; #[cfg(target_pointer_width = "32")] #[test] fn test_size() { assert_size!(Confirm, 36); } #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(Confirm, 72); } #[test] fn test_simple() { let elem: Element = "" .parse() .unwrap(); let confirm = Confirm::try_from(elem).unwrap(); assert_eq!(confirm.method, "GET"); assert_eq!(confirm.id, "a7374jnjlalasdf82"); assert_eq!( confirm.url, "https://files.shakespeare.lit:9345/missive.html" ); } #[test] fn test_serialise() { let confirm = Confirm { method: "GET".to_string(), url: "https://files.shakespeare.lit/".to_string(), id: "sesame".to_string(), }; let elem: Element = confirm.into(); assert!(elem.is("confirm", ns::HTTP_AUTH)); } } xmpp-parsers-0.22.0/src/csi.rs000064400000000000000000000033431046102023000142640ustar 00000000000000// Copyright (c) 2019 Emmanuel Gil Peyrot // // 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 xso::{AsXml, FromXml}; use crate::ns; /// Stream:feature sent by the server to advertise it supports CSI. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::CSI, name = "csi")] pub struct Feature; /// Client indicates it is inactive. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::CSI, name = "inactive")] pub struct Inactive; /// Client indicates it is active again. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::CSI, name = "active")] pub struct Active; #[cfg(test)] mod tests { use super::*; use crate::ns; use minidom::Element; #[test] fn test_size() { assert_size!(Feature, 0); assert_size!(Inactive, 0); assert_size!(Active, 0); } #[test] fn parsing() { let elem: Element = "".parse().unwrap(); Feature::try_from(elem).unwrap(); let elem: Element = "".parse().unwrap(); Inactive::try_from(elem).unwrap(); let elem: Element = "".parse().unwrap(); Active::try_from(elem).unwrap(); } #[test] fn serialising() { let elem: Element = Feature.into(); assert!(elem.is("csi", ns::CSI)); let elem: Element = Inactive.into(); assert!(elem.is("inactive", ns::CSI)); let elem: Element = Active.into(); assert!(elem.is("active", ns::CSI)); } } xmpp-parsers-0.22.0/src/data_forms.rs000064400000000000000000000540611046102023000156300ustar 00000000000000// Copyright (c) 2017 Emmanuel Gil Peyrot // // 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 xso::{error::Error, AsXml, FromXml}; use crate::data_forms_validate::Validate; use crate::media_element::MediaElement; use crate::ns; /// Represents one of the possible values for a list- field. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::DATA_FORMS, name = "option")] pub struct Option_ { /// The optional label to be displayed to the user for this option. #[xml(attribute(default))] pub label: Option, /// The value returned to the server when selecting this option. #[xml(extract(fields(text)))] pub value: String, } generate_attribute!( /// The type of a [field](struct.Field.html) element. FieldType, "type", { /// This field can only take the values "0" or "false" for a false /// value, and "1" or "true" for a true value. Boolean => "boolean", /// This field describes data, it must not be sent back to the /// requester. Fixed => "fixed", /// This field is hidden, it should not be displayed to the user but /// should be sent back to the requester. Hidden => "hidden", /// This field accepts one or more [JIDs](../../jid/struct.Jid.html). /// A client may want to let the user autocomplete them based on their /// contacts list for instance. JidMulti => "jid-multi", /// This field accepts one [JID](../../jid/struct.Jid.html). A client /// may want to let the user autocomplete it based on their contacts /// list for instance. JidSingle => "jid-single", /// This field accepts one or more values from the list provided as /// [options](struct.Option_.html). ListMulti => "list-multi", /// This field accepts one value from the list provided as /// [options](struct.Option_.html). ListSingle => "list-single", /// This field accepts one or more free form text lines. TextMulti => "text-multi", /// This field accepts one free form password, a client should hide it /// in its user interface. TextPrivate => "text-private", /// This field accepts one free form text line. TextSingle => "text-single", }, Default = TextSingle ); fn validate_field(field: &mut Field) -> Result<(), Error> { if field.type_ != FieldType::Fixed && field.var.is_none() { return Err(Error::Other("Required attribute 'var' missing.")); } if !field.is_list() && field.options.len() > 0 { return Err(Error::Other("Option element found in non-list field.")); } Ok(()) } /// Represents a field in a [data form](struct.DataForm.html). #[derive(FromXml, AsXml, Debug, Clone, PartialEq)] #[xml(namespace = ns::DATA_FORMS, name = "field", deserialize_callback = validate_field)] pub struct Field { /// The unique identifier for this field, in the form. #[xml(attribute(default))] pub var: Option, /// The type of this field. #[xml(attribute(name = "type", default))] pub type_: FieldType, /// The label to be possibly displayed to the user for this field. #[xml(attribute(default))] pub label: Option, /// The form will be rejected if this field isn’t present. #[xml(flag)] pub required: bool, /// The natural-language description of the field, intended for presentation in a user-agent #[xml(extract(default, fields(text(type_ = String))))] pub desc: Option, /// A list of allowed values. #[xml(child(n = ..))] pub options: Vec, /// The values provided for this field. #[xml(extract(n = .., name = "value", fields(text(type_ = String))))] pub values: Vec, /// A list of media related to this field. #[xml(child(n = ..))] pub media: Vec, /// Validation rules for this field. #[xml(child(default))] pub validate: Option, } impl Field { /// Create a new Field, of the given var and type. pub fn new(var: &str, type_: FieldType) -> Field { Field { var: Some(String::from(var)), type_, label: None, required: false, desc: None, options: Vec::new(), media: Vec::new(), values: Vec::new(), validate: None, } } /// Set only one value in this Field. pub fn with_value(mut self, value: &str) -> Field { self.values.push(String::from(value)); self } /// Create a text-single Field with the given var and unique value. pub fn text_single(var: &str, value: &str) -> Field { Field::new(var, FieldType::TextSingle).with_value(value) } fn is_list(&self) -> bool { self.type_ == FieldType::ListSingle || self.type_ == FieldType::ListMulti } /// Return true if this field is a valid form type specifier as per /// [XEP-0068](https://xmpp.org/extensions/xep-0068.html). /// /// This function requires knowledge of the form's type attribute as the /// criteria differ slightly among form types. pub fn is_form_type(&self, ty: &DataFormType) -> bool { // 1. A field must have the var FORM_TYPE if self.var.as_deref() != Some("FORM_TYPE") { return false; } match ty { // https://xmpp.org/extensions/xep-0068.html#usecases-incorrect // > If the FORM_TYPE field is not hidden in a form with // > type="form" or type="result", it MUST be ignored as a context // > indicator. DataFormType::Form | DataFormType::Result_ => self.type_ == FieldType::Hidden, // https://xmpp.org/extensions/xep-0068.html#impl // > Data forms with the type "submit" are free to omit any // > explicit field type declaration (as per Data Forms (XEP-0004) // > § 3.2), as the type is implied by the corresponding // > "form"-type data form. As consequence, implementations MUST // > treat a FORM_TYPE field without an explicit type attribute, // > in data forms of type "submit", as the FORM_TYPE field with // > the special meaning defined herein. DataFormType::Submit => matches!(self.type_, FieldType::Hidden | FieldType::TextSingle), // XEP-0068 does not explicitly mention cancel type forms. // However, XEP-0004 states: // > a data form of type "cancel" SHOULD NOT contain any // > elements. // thus we ignore those. DataFormType::Cancel => false, } } } generate_attribute!( /// Represents the type of a [data form](struct.DataForm.html). DataFormType, "type", { /// This is a cancel request for a prior type="form" data form. Cancel => "cancel", /// This is a request for the recipient to fill this form and send it /// back as type="submit". Form => "form", /// This is a result form, which contains what the requester asked for. Result_ => "result", /// This is a complete response to a form received before. Submit => "submit", } ); /// This is a form to be sent to another entity for filling. #[derive(FromXml, AsXml, Debug, Clone, PartialEq)] #[xml(namespace = ns::DATA_FORMS, name = "x", deserialize_callback = patch_form)] pub struct DataForm { /// The type of this form, telling the other party which action to execute. #[xml(attribute = "type")] pub type_: DataFormType, /// The title of this form. #[xml(extract(fields(text(type_ = String)), default))] pub title: Option, /// The instructions given with this form. #[xml(extract(fields(text(type_ = String)), default))] pub instructions: Option, /// A list of fields comprising this form. #[xml(child(n = ..))] pub fields: Vec, } fn patch_form(form: &mut DataForm) -> Result<(), Error> { // Sort the FORM_TYPE field, if any, to the first position. let mut form_type_index = None; for (i, field) in form.fields.iter().enumerate() { if field.is_form_type(&form.type_) { if form_type_index.is_some() { return Err(Error::Other("More than one FORM_TYPE in a data form.")); } if field.values.len() != 1 { return Err(Error::Other("Wrong number of values in FORM_TYPE.")); } form_type_index = Some(i); } } if let Some(index) = form_type_index { let field = form.fields.remove(index); form.fields.insert(0, field); } Ok(()) } impl DataForm { /// Create a new DataForm. pub fn new(type_: DataFormType, form_type: &str, fields: Vec) -> DataForm { let mut form = DataForm { type_, title: None, instructions: None, fields, }; form.set_form_type(form_type.to_owned()); form } /// Return the value of the `FORM_TYPE` field, if any. /// /// An easy accessor for the FORM_TYPE of this form, see /// [XEP-0068](https://xmpp.org/extensions/xep-0068.html) for more /// information. pub fn form_type(&self) -> Option<&str> { for field in self.fields.iter() { if field.is_form_type(&self.type_) { return field.values.first().map(|x| x.as_str()); } } None } /// Create or modify the `FORM_TYPE` field. /// /// If the form has no `FORM_TYPE` field, this function creates a new /// field and inserts it at the beginning of the field list. Otherwise, it /// returns a mutable reference to the existing field. /// /// # Panics /// /// If the type of the form is [`DataFormType::Cancel`], this function /// panics. Such forms should not have any fields and thus no form type. pub fn set_form_type(&mut self, ty: String) -> &mut Field { if self.type_ == DataFormType::Cancel { panic!("cannot add FORM_TYPE field to type='cancel' form"); } // Awkward index enum to work around borrowck limitations. let mut index = None; for (i, field) in self.fields.iter().enumerate() { if field.is_form_type(&self.type_) { index = Some(i); break; } } let field = if let Some(index) = index { &mut self.fields[index] } else { let field = Field::new("FORM_TYPE", FieldType::Hidden); assert!(field.is_form_type(&self.type_)); self.fields.insert(0, field); &mut self.fields[0] }; field.values.clear(); field.values.push(ty); field } } #[cfg(test)] mod tests { use super::*; use crate::data_forms_validate::{Datatype, Validate}; use minidom::Element; use xso::error::{Error, FromElementError}; #[cfg(target_pointer_width = "32")] #[test] fn test_size() { assert_size!(Option_, 24); assert_size!(FieldType, 1); assert_size!(Field, 140); assert_size!(DataFormType, 1); assert_size!(DataForm, 40); } #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(Option_, 48); assert_size!(FieldType, 1); assert_size!(Field, 264); assert_size!(DataFormType, 1); assert_size!(DataForm, 80); } #[test] fn test_simple() { let elem: Element = "".parse().unwrap(); let form = DataForm::try_from(elem).unwrap(); assert_eq!(form.type_, DataFormType::Result_); assert!(form.form_type().is_none()); assert!(form.fields.is_empty()); } #[test] fn test_missing_var() { let elem: Element = "" .parse() .unwrap(); let error = DataForm::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!(message, "Required attribute 'var' missing."); } #[test] fn test_fixed_field() { let elem: Element = "Section 1: Bot Info" .parse() .unwrap(); let form = DataForm::try_from(elem).unwrap(); assert_eq!(form.type_, DataFormType::Form); assert!(form.form_type().is_none()); assert_eq!( form.fields, vec![Field { var: None, type_: FieldType::Fixed, label: None, required: false, desc: None, options: vec![], values: vec!["Section 1: Bot Info".to_string()], media: vec![], validate: None, }] ); } #[test] fn test_desc() { let elem: Element = "Tell all your friends about your new bot!" .parse() .unwrap(); let form = DataForm::try_from(elem).unwrap(); assert_eq!(form.type_, DataFormType::Form); assert!(form.form_type().is_none()); assert_eq!( form.fields, vec![Field { var: Some("invitelist".to_string()), type_: FieldType::JidMulti, label: Some("People to invite".to_string()), required: false, desc: Some("Tell all your friends about your new bot!".to_string()), options: vec![], values: vec![], media: vec![], validate: None, }] ); } #[test] fn test_validate() { let elem: Element = r#" 2003-10-06T11:22:00-07:00 "# .parse() .unwrap(); let form = DataForm::try_from(elem).unwrap(); assert_eq!(form.type_, DataFormType::Form); assert!(form.form_type().is_none()); assert_eq!( form.fields, vec![Field { var: Some("evt.date".to_string()), type_: FieldType::TextSingle, label: Some("Event Date/Time".to_string()), required: false, desc: None, options: vec![], values: vec!["2003-10-06T11:22:00-07:00".to_string()], media: vec![], validate: Some(Validate { datatype: Some(Datatype::DateTime), method: None, list_range: None, }), }] ); } #[test] fn test_invalid_field() { let elem: Element = "".parse().unwrap(); let error = Field::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!(message, "Option element found in non-list field."); } #[test] fn test_invalid() { let elem: Element = "".parse().unwrap(); let error = DataForm::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!( message, "Required attribute field 'type_' on DataForm element missing." ); let elem: Element = "".parse().unwrap(); let error = DataForm::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::TextParseError(string)) => string, other => panic!("unexpected result: {:?}", other), }; assert_eq!(message.to_string(), "Unknown value for 'type' attribute."); } #[test] fn test_wrong_child() { let elem: Element = "" .parse() .unwrap(); let error = DataForm::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!(message, "Unknown child in DataForm element."); } #[test] fn option() { let elem: Element = "" .parse() .unwrap(); let option = Option_::try_from(elem).unwrap(); assert_eq!(&option.label.unwrap(), "Coucou !"); assert_eq!(&option.value, "coucou"); let elem: Element = "".parse().unwrap(); let error = Option_::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!( message, "Option_ element must not have more than one child in field 'value'." ); } #[test] fn test_ignore_form_type_field_if_field_type_mismatches_in_form_typed_forms() { // https://xmpp.org/extensions/xep-0068.html#usecases-incorrect // […] it MUST be ignored as a context indicator let elem: Element = "foo".parse().unwrap(); match DataForm::try_from(elem) { Ok(form) => { match form.form_type() { None => (), other => panic!("unexpected extracted form type: {:?}", other), }; } other => panic!("unexpected result: {:?}", other), } } #[test] fn test_ignore_form_type_field_if_field_type_mismatches_in_result_typed_forms() { // https://xmpp.org/extensions/xep-0068.html#usecases-incorrect // […] it MUST be ignored as a context indicator let elem: Element = "foo".parse().unwrap(); match DataForm::try_from(elem) { Ok(form) => { match form.form_type() { None => (), other => panic!("unexpected extracted form type: {:?}", other), }; } other => panic!("unexpected result: {:?}", other), } } #[test] fn test_accept_form_type_field_without_type_attribute_in_submit_typed_forms() { let elem: Element = "foo".parse().unwrap(); match DataForm::try_from(elem) { Ok(form) => { match form.form_type() { Some(ty) => assert_eq!(ty, "foo"), other => panic!("unexpected extracted form type: {:?}", other), }; } other => panic!("unexpected result: {:?}", other), } } #[test] fn test_accept_form_type_field_with_type_hidden_in_submit_typed_forms() { let elem: Element = "foo".parse().unwrap(); match DataForm::try_from(elem) { Ok(form) => { match form.form_type() { Some(ty) => assert_eq!(ty, "foo"), other => panic!("unexpected extracted form type: {:?}", other), }; } other => panic!("unexpected result: {:?}", other), } } #[test] fn test_accept_form_type_field_with_type_hidden_in_result_typed_forms() { let elem: Element = "foo".parse().unwrap(); match DataForm::try_from(elem) { Ok(form) => { match form.form_type() { Some(ty) => assert_eq!(ty, "foo"), other => panic!("unexpected extracted form type: {:?}", other), }; } other => panic!("unexpected result: {:?}", other), } } #[test] fn test_accept_form_type_field_with_type_hidden_in_form_typed_forms() { let elem: Element = "foo".parse().unwrap(); match DataForm::try_from(elem) { Ok(form) => { match form.form_type() { Some(ty) => assert_eq!(ty, "foo"), other => panic!("unexpected extracted form type: {:?}", other), }; } other => panic!("unexpected result: {:?}", other), } } } xmpp-parsers-0.22.0/src/data_forms_validate.rs000064400000000000000000000401031046102023000174710ustar 00000000000000// 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 `` element shall contain a `` child element. Using `` validation, /// the form interpreter MUST follow the validation rules of the datatype (if understood) and /// the field type. /// /// #[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 `` /// element shall contain an `` child element. The `` 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. /// /// #[xml(name = "open")] Open, /// To indicate that the value should fall within a certain range, the `` element shall /// contain a `` child element. The 'min' and 'max' attributes of the `` 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 `` 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. /// /// #[xml(name = "range")] Range { /// The 'min' attribute specifies the minimum allowable value. #[xml(attribute(default))] min: Option, /// The 'max' attribute specifies the maximum allowable value. #[xml(attribute(default))] max: Option, }, /// To indicate that the value should be restricted to a regular expression, the `` /// element shall contain a `` 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 `` element MUST contain /// character data only. /// /// #[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, /// The 'max' attribute specifies the maximum allowable number of selected/entered values. #[xml(attribute(default))] pub max: Option, } /// 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 /// /// #[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, /// The validation method. If no validation method is specified, form processors MUST /// assume `` validation. The `` 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 ``) MUST imply the same behavior as ``, with the additional constraints /// defined by that method. /// /// #[xml(child(default))] pub method: Option, /// For "list-multi", validation can indicate (via the `` 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 `` element SHOULD be included only when the `` is of type "list-multi" /// and SHOULD be ignored otherwise. /// /// The `` 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. /// /// #[xml(child(default))] pub list_range: Option, } impl FromStr for Datatype { type Err = DatatypeError; fn from_str(s: &str) -> Result { 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 { Some(self.to_string()) } } impl FromXmlText for Datatype { fn from_xml_text(s: String) -> Result { s.parse().map_err(Error::text_parse_error) } } impl AsXmlText for Datatype { fn as_xml_text(&self) -> Result, 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::(), ); assert_eq!( "xs:".parse::(), 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 { datatype: None, method: None, list_range: None, }, ), ( r#""#, Validate { datatype: Some(Datatype::String), method: Some(Method::Basic), list_range: Some(ListRange { min: Some(1), max: Some(3), }), }, ), ( r#"([0-9]{3})-([0-9]{2})-([0-9]{4})"#, 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 { 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::() .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#""#, r#""#, ]; for case in cases { let element = case .parse::() .expect(&format!("Failed to parse {}", case)); assert!(Validate::try_from(element).is_err()); } } } xmpp-parsers-0.22.0/src/date.rs000064400000000000000000000132621046102023000144240ustar 00000000000000// Copyright (c) 2017 Emmanuel Gil Peyrot // // 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::str::FromStr; use xso::{error::Error, AsXmlText, FromXmlText, TextCodec}; use chrono::{DateTime as ChronoDateTime, FixedOffset, Utc}; use minidom::{IntoAttributeValue, Node}; /// Text codec for /// [XEP-0082](https://xmpp.org/extensions/xep-0082.html)-compliant formatting /// of dates and times. pub struct Xep0082; impl TextCodec> for Xep0082 { fn decode(&self, s: String) -> Result, Error> { ChronoDateTime::parse_from_rfc3339(&s).map_err(Error::text_parse_error) } fn encode<'x>( &self, value: &'x ChronoDateTime, ) -> Result>, Error> { if value.offset().utc_minus_local() == 0 { Ok(Some(Cow::Owned( value.format("%Y-%m-%dT%H:%M:%SZ").to_string(), ))) } else { Ok(Some(Cow::Owned(value.to_rfc3339()))) } } } impl TextCodec> for Xep0082 { fn decode(&self, s: String) -> Result, Error> { Ok(ChronoDateTime::::parse_from_rfc3339(&s) .map_err(Error::text_parse_error)? .into()) } fn encode<'x>(&self, value: &'x ChronoDateTime) -> Result>, Error> { Ok(Some(Cow::Owned( value.format("%Y-%m-%dT%H:%M:%SZ").to_string(), ))) } } impl TextCodec> for Xep0082 where Xep0082: TextCodec, { fn decode(&self, s: String) -> Result, Error> { Ok(Some(self.decode(s)?)) } fn encode<'x>(&self, value: &'x Option) -> Result>, Error> { value .as_ref() .and_then(|x| self.encode(x).transpose()) .transpose() } } /// Implements the DateTime profile of XEP-0082, which represents a /// non-recurring moment in time, with an accuracy of seconds or fraction of /// seconds, and includes a timezone. #[derive(Debug, Clone, PartialEq)] pub struct DateTime(pub ChronoDateTime); impl DateTime { /// Retrieves the associated timezone. pub fn timezone(&self) -> FixedOffset { self.0.timezone() } /// Returns a new `DateTime` with a different timezone. pub fn with_timezone(&self, tz: FixedOffset) -> DateTime { DateTime(self.0.with_timezone(&tz)) } /// Formats this `DateTime` with the specified format string. pub fn format(&self, fmt: &str) -> String { format!("{}", self.0.format(fmt)) } } impl FromStr for DateTime { type Err = chrono::ParseError; fn from_str(s: &str) -> Result { Ok(DateTime(ChronoDateTime::parse_from_rfc3339(s)?)) } } impl FromXmlText for DateTime { fn from_xml_text(s: String) -> Result { s.parse().map_err(Error::text_parse_error) } } impl AsXmlText for DateTime { fn as_xml_text(&self) -> Result, Error> { Ok(Cow::Owned(self.0.to_rfc3339())) } } impl IntoAttributeValue for DateTime { fn into_attribute_value(self) -> Option { Some(self.0.to_rfc3339()) } } impl From for Node { fn from(date: DateTime) -> Node { Node::Text(date.0.to_rfc3339()) } } #[cfg(test)] mod tests { use super::*; use chrono::{Datelike, Timelike}; // DateTime’s size doesn’t depend on the architecture. #[test] fn test_size() { assert_size!(DateTime, 16); } #[test] fn test_simple() { let date: DateTime = "2002-09-10T23:08:25Z".parse().unwrap(); assert_eq!(date.0.year(), 2002); assert_eq!(date.0.month(), 9); assert_eq!(date.0.day(), 10); assert_eq!(date.0.hour(), 23); assert_eq!(date.0.minute(), 08); assert_eq!(date.0.second(), 25); assert_eq!(date.0.nanosecond(), 0); assert_eq!(date.0.timezone(), FixedOffset::east_opt(0).unwrap()); } #[test] fn test_invalid_date() { // There is no thirteenth month. let error = DateTime::from_str("2017-13-01T12:23:34Z").unwrap_err(); assert_eq!(error.to_string(), "input is out of range"); // Timezone ≥24:00 aren’t allowed. let error = DateTime::from_str("2017-05-27T12:11:02+25:00").unwrap_err(); assert_eq!(error.to_string(), "input is out of range"); // Timezone without the : separator aren’t allowed. let error = DateTime::from_str("2017-05-27T12:11:02+0100").unwrap_err(); assert_eq!(error.to_string(), "input contains invalid characters"); // No seconds, error message could be improved. let error = DateTime::from_str("2017-05-27T12:11+01:00").unwrap_err(); assert_eq!(error.to_string(), "input contains invalid characters"); // TODO: maybe we’ll want to support this one, as per XEP-0082 §4. let error = DateTime::from_str("20170527T12:11:02+01:00").unwrap_err(); assert_eq!(error.to_string(), "input contains invalid characters"); // No timezone. let error = DateTime::from_str("2017-05-27T12:11:02").unwrap_err(); assert_eq!(error.to_string(), "premature end of input"); } #[test] fn test_serialise() { let date = DateTime(ChronoDateTime::parse_from_rfc3339("2017-05-21T20:19:55+01:00").unwrap()); let attr = date.into_attribute_value(); assert_eq!(attr, Some(String::from("2017-05-21T20:19:55+01:00"))); } } xmpp-parsers-0.22.0/src/delay.rs000064400000000000000000000073371046102023000146130ustar 00000000000000// Copyright (c) 2017 Emmanuel Gil Peyrot // // 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 xso::{text::EmptyAsNone, AsXml, FromXml}; use crate::date::DateTime; use crate::message::MessagePayload; use crate::ns; use crate::presence::PresencePayload; use jid::Jid; /// Notes when and by whom a message got stored for later delivery. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::DELAY, name = "delay")] pub struct Delay { /// The entity which delayed this message. #[xml(attribute(default))] pub from: Option, /// The time at which this message got stored. #[xml(attribute)] pub stamp: DateTime, /// The optional reason this message got delayed. #[xml(text = EmptyAsNone)] pub data: Option, } impl MessagePayload for Delay {} impl PresencePayload for Delay {} #[cfg(test)] mod tests { use super::*; use core::str::FromStr; use jid::BareJid; use minidom::Element; use xso::error::{Error, FromElementError}; #[cfg(target_pointer_width = "32")] #[test] fn test_size() { assert_size!(Delay, 44); } #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(Delay, 72); } #[test] fn test_simple() { let elem: Element = "" .parse() .unwrap(); let delay = Delay::try_from(elem).unwrap(); assert_eq!(delay.from.unwrap(), BareJid::new("capulet.com").unwrap()); assert_eq!( delay.stamp, DateTime::from_str("2002-09-10T23:08:25Z").unwrap() ); assert_eq!(delay.data, None); } #[test] fn test_unknown() { let elem: Element = "" .parse() .unwrap(); let error = Delay::try_from(elem.clone()).unwrap_err(); let returned_elem = match error { FromElementError::Mismatch(elem) => elem, _ => panic!(), }; assert_eq!(elem, returned_elem); } #[test] #[cfg_attr(feature = "disable-validation", should_panic = "Result::unwrap_err")] fn test_invalid_child() { let elem: Element = "" .parse() .unwrap(); let error = Delay::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!(message, "Unknown child in Delay element."); } #[test] fn test_serialise() { let elem: Element = "" .parse() .unwrap(); let delay = Delay { from: None, stamp: DateTime::from_str("2002-09-10T23:08:25Z").unwrap(), data: None, }; let elem2 = delay.into(); assert_eq!(elem, elem2); } #[test] fn test_serialise_data() { let elem: Element = "Reason".parse().unwrap(); let delay = Delay { from: Some(Jid::new("juliet@example.org").unwrap()), stamp: DateTime::from_str("2002-09-10T23:08:25Z").unwrap(), data: Some(String::from("Reason")), }; let elem2 = delay.into(); assert_eq!(elem, elem2); } } xmpp-parsers-0.22.0/src/disco.rs000064400000000000000000000360631046102023000146140ustar 00000000000000// Copyright (c) 2017 Emmanuel Gil Peyrot // // 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 xso::{AsXml, FromXml}; use crate::data_forms::DataForm; use crate::iq::{IqGetPayload, IqResultPayload}; use crate::ns; use crate::rsm::{SetQuery, SetResult}; use jid::Jid; /// Structure representing a `` element. /// /// It should only be used in an ``, as it can only represent /// the request, and not a result. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::DISCO_INFO, name = "query")] pub struct DiscoInfoQuery { /// Node on which we are doing the discovery. #[xml(attribute(default))] pub node: Option, } impl IqGetPayload for DiscoInfoQuery {} /// Structure representing a `` element. #[derive(FromXml, AsXml, Debug, Clone, PartialEq, Eq, Hash)] #[xml(namespace = ns::DISCO_INFO, name = "feature")] pub struct Feature { /// Namespace of the feature we want to represent. #[xml(attribute)] pub var: String, } impl Feature { /// Create a new `` with the according `@var`. pub fn new>(var: S) -> Feature { Feature { var: var.into() } } } /// Structure representing an `` element. #[derive(FromXml, AsXml, Debug, Clone, PartialEq, Eq, Hash)] #[xml(namespace = ns::DISCO_INFO, name = "identity")] pub struct Identity { /// Category of this identity. // TODO: use an enum here. #[xml(attribute)] pub category: String, /// Type of this identity. // TODO: use an enum here. #[xml(attribute = "type")] pub type_: String, /// Lang of the name of this identity. #[xml(lang(default))] pub lang: Option, /// Name of this identity. #[xml(attribute(default))] pub name: Option, } impl Identity { /// Create a new ``. pub fn new(category: C, type_: T, lang: L, name: N) -> Identity where C: Into, T: Into, L: Into, N: Into, { Identity { category: category.into(), type_: type_.into(), lang: Some(lang.into()), name: Some(name.into()), } } /// Create a new `` without a name. pub fn new_anonymous(category: C, type_: T) -> Identity where C: Into, T: Into, { Identity { category: category.into(), type_: type_.into(), lang: None, name: None, } } } /// Structure representing a `` element. /// /// It should only be used in an ``, as it can only /// represent the result, and not a request. #[derive(FromXml, AsXml, Debug, Clone)] #[xml(namespace = ns::DISCO_INFO, name = "query")] pub struct DiscoInfoResult { /// Node on which we have done this discovery. #[xml(attribute(default))] pub node: Option, /// List of identities exposed by this entity. #[xml(child(n = ..))] pub identities: Vec, /// List of features supported by this entity. #[xml(child(n = ..))] pub features: Vec, /// List of extensions reported by this entity. #[xml(child(n = ..))] pub extensions: Vec, } impl IqResultPayload for DiscoInfoResult {} /// Structure representing a `` element. /// /// It should only be used in an ``, as it can only represent /// the request, and not a result. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::DISCO_ITEMS, name = "query")] pub struct DiscoItemsQuery { /// Node on which we are doing the discovery. #[xml(attribute(default))] pub node: Option, /// Optional paging via Result Set Management #[xml(child(default))] pub rsm: Option, } impl IqGetPayload for DiscoItemsQuery {} /// Structure representing an `` element. #[derive(FromXml, AsXml, Debug, Clone, PartialEq)] #[xml(namespace = ns::DISCO_ITEMS, name = "item")] pub struct Item { /// JID of the entity pointed by this item. #[xml(attribute)] pub jid: Jid, /// Node of the entity pointed by this item. #[xml(attribute(default))] pub node: Option, /// Name of the entity pointed by this item. #[xml(attribute(default))] pub name: Option, } /// Structure representing a `` element. /// /// It should only be used in an ``, as it can only /// represent the result, and not a request. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::DISCO_ITEMS, name = "query")] pub struct DiscoItemsResult { /// Node on which we have done this discovery. #[xml(attribute(default))] pub node: Option, /// List of items pointed by this entity. #[xml(child(n = ..))] pub items: Vec, /// Optional paging via Result Set Management #[xml(child(default))] pub rsm: Option, } impl IqResultPayload for DiscoItemsResult {} #[cfg(test)] mod tests { use super::*; use jid::BareJid; use minidom::Element; use xso::error::{Error, FromElementError}; #[cfg(target_pointer_width = "32")] #[test] fn test_size() { assert_size!(Identity, 48); assert_size!(Feature, 12); assert_size!(DiscoInfoQuery, 12); assert_size!(DiscoInfoResult, 48); assert_size!(Item, 40); assert_size!(DiscoItemsQuery, 52); assert_size!(DiscoItemsResult, 64); } #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(Identity, 96); assert_size!(Feature, 24); assert_size!(DiscoInfoQuery, 24); assert_size!(DiscoInfoResult, 96); assert_size!(Item, 80); assert_size!(DiscoItemsQuery, 104); assert_size!(DiscoItemsResult, 128); } #[test] fn test_simple() { let elem: Element = "".parse().unwrap(); let query = DiscoInfoResult::try_from(elem).unwrap(); assert!(query.node.is_none()); assert_eq!(query.identities.len(), 1); assert_eq!(query.features.len(), 1); assert!(query.extensions.is_empty()); } #[test] fn test_identity_after_feature() { let elem: Element = "".parse().unwrap(); let query = DiscoInfoResult::try_from(elem).unwrap(); assert_eq!(query.identities.len(), 1); assert_eq!(query.features.len(), 1); assert!(query.extensions.is_empty()); } #[test] fn test_feature_after_dataform() { let elem: Element = "coucou".parse().unwrap(); let query = DiscoInfoResult::try_from(elem).unwrap(); assert_eq!(query.identities.len(), 1); assert_eq!(query.features.len(), 1); assert_eq!(query.extensions.len(), 1); } #[test] fn test_extension() { let elem: Element = "example".parse().unwrap(); let elem1 = elem.clone(); let query = DiscoInfoResult::try_from(elem).unwrap(); assert!(query.node.is_none()); assert_eq!(query.identities.len(), 1); assert_eq!(query.features.len(), 1); assert_eq!(query.extensions.len(), 1); assert_eq!(query.extensions[0].form_type(), Some("example")); let elem2 = query.into(); assert_eq!(elem1, elem2); } #[test] #[cfg_attr(feature = "disable-validation", should_panic = "Result::unwrap_err")] fn test_invalid() { let elem: Element = "" .parse() .unwrap(); let error = DiscoInfoResult::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!(message, "Unknown child in DiscoInfoResult element."); } #[test] fn test_invalid_identity() { let elem: Element = "" .parse() .unwrap(); let error = DiscoInfoResult::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!( message, "Required attribute field 'category' on Identity element missing." ); let elem: Element = "" .parse() .unwrap(); let error = DiscoInfoResult::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!( message, "Required attribute field 'category' on Identity element missing." ); let elem: Element = "".parse().unwrap(); let error = DiscoInfoResult::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!( message, "Required attribute field 'type_' on Identity element missing." ); } #[test] fn test_invalid_feature() { let elem: Element = "" .parse() .unwrap(); let error = DiscoInfoResult::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!( message, "Required attribute field 'var' on Feature element missing." ); } // TODO: We stopped validating that there are enough identities and features in this result, // this is a limitation of xso which accepts n = .. only, and not n = 1.., so let’s wait until // xso implements this to reenable this test. #[test] #[ignore] fn test_invalid_result() { let elem: Element = "" .parse() .unwrap(); let error = DiscoInfoResult::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!( message, "There must be at least one identity in disco#info." ); let elem: Element = "".parse().unwrap(); let error = DiscoInfoResult::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!(message, "There must be at least one feature in disco#info."); } #[test] fn test_simple_items() { let elem: Element = "" .parse() .unwrap(); let query = DiscoItemsQuery::try_from(elem).unwrap(); assert!(query.node.is_none()); let elem: Element = "" .parse() .unwrap(); let query = DiscoItemsQuery::try_from(elem).unwrap(); assert_eq!(query.node, Some(String::from("coucou"))); } #[test] fn test_simple_items_result() { let elem: Element = "" .parse() .unwrap(); let query = DiscoItemsResult::try_from(elem).unwrap(); assert!(query.node.is_none()); assert!(query.items.is_empty()); let elem: Element = "" .parse() .unwrap(); let query = DiscoItemsResult::try_from(elem).unwrap(); assert_eq!(query.node, Some(String::from("coucou"))); assert!(query.items.is_empty()); } #[test] fn test_answers_items_result() { let elem: Element = "".parse().unwrap(); let query = DiscoItemsResult::try_from(elem).unwrap(); let elem2 = Element::from(query); let query = DiscoItemsResult::try_from(elem2).unwrap(); assert_eq!(query.items.len(), 2); assert_eq!(query.items[0].jid, BareJid::new("component").unwrap()); assert_eq!(query.items[0].node, None); assert_eq!(query.items[0].name, None); assert_eq!(query.items[1].jid, BareJid::new("component2").unwrap()); assert_eq!(query.items[1].node, Some(String::from("test"))); assert_eq!(query.items[1].name, Some(String::from("A component"))); } // WORKAROUND FOR PROSODY BUG 1664, DO NOT REMOVE BEFORE 2028-12-17 (5 YEARS AFTER FIX) // https://issues.prosody.im/1664 // See also: // https://gitlab.com/xmpp-rs/xmpp-rs/-/issues/128 // https://gitlab.com/xmpp-rs/xmpp-rs/-/merge_requests/302 #[test] fn test_missing_disco_info_feature_workaround() { let elem: Element = "".parse().unwrap(); let query = DiscoInfoResult::try_from(elem).unwrap(); assert_eq!(query.identities.len(), 1); assert_eq!(query.features.len(), 1); assert!(query.extensions.is_empty()); } } xmpp-parsers-0.22.0/src/ecaps2.rs000064400000000000000000000570301046102023000146650ustar 00000000000000// Copyright (c) 2017 Emmanuel Gil Peyrot // // 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 xso::{AsXml, FromXml}; use crate::data_forms::DataForm; use crate::disco::{DiscoInfoQuery, DiscoInfoResult, Feature, Identity}; use crate::hashes::{Algo, Hash}; use crate::ns; use crate::presence::PresencePayload; use base64::{engine::general_purpose::STANDARD as Base64, Engine}; use blake2::Blake2bVar; use digest::{Digest, Update, VariableOutput}; use sha2::{Sha256, Sha512}; use sha3::{Sha3_256, Sha3_512}; use xso::error::Error; /// Represents a set of capability hashes, all of them must correspond to /// the same input [disco#info](../disco/struct.DiscoInfoResult.html), /// using different [algorithms](../hashes/enum.Algo.html). #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::ECAPS2, name = "c")] pub struct ECaps2 { /// Hashes of the [disco#info](../disco/struct.DiscoInfoResult.html). #[xml(child(n = ..))] pub hashes: Vec, } impl PresencePayload for ECaps2 {} impl ECaps2 { /// Create an ECaps2 element from a list of hashes. pub fn new(hashes: Vec) -> ECaps2 { ECaps2 { hashes } } } fn compute_item(field: &str) -> Vec { let mut bytes = field.as_bytes().to_vec(); bytes.push(0x1f); bytes } fn compute_items<'x, T: 'x, I: IntoIterator, F: Fn(&'x T) -> Vec>( things: I, separator: u8, encode: F, ) -> Vec { let mut string: Vec = vec![]; let mut accumulator: Vec> = vec![]; for thing in things.into_iter() { let bytes = encode(thing); accumulator.push(bytes); } // This works using the expected i;octet collation. accumulator.sort(); for mut bytes in accumulator { string.append(&mut bytes); } string.push(separator); string } fn compute_features(features: &[Feature]) -> Vec { compute_items(features, 0x1c, |feature| compute_item(&feature.var)) } fn compute_identities(identities: &[Identity]) -> Vec { compute_items(identities, 0x1c, |identity| { let mut bytes = compute_item(&identity.category); bytes.append(&mut compute_item(&identity.type_)); bytes.append(&mut compute_item( identity.lang.as_deref().unwrap_or_default(), )); bytes.append(&mut compute_item( identity.name.as_deref().unwrap_or_default(), )); bytes.push(0x1e); bytes }) } fn compute_extensions(extensions: &[DataForm]) -> Result, Error> { for extension in extensions { if extension.form_type().is_none() { return Err(Error::Other("Missing FORM_TYPE in extension.")); } } Ok(compute_items(extensions, 0x1c, |extension| { let mut bytes = compute_item("FORM_TYPE"); bytes.append(&mut compute_item( if let Some(form_type) = extension.form_type() { form_type } else { unreachable!() }, )); bytes.push(0x1e); bytes.append(&mut compute_items( extension .fields .iter() .filter(|field| field.var.as_deref().unwrap_or("") != "FORM_TYPE"), 0x1d, |field| { let mut bytes = vec![]; if let Some(var) = &field.var { bytes.append(&mut compute_item(var)); } bytes.append(&mut compute_items(&field.values, 0x1e, |value| { compute_item(value) })); bytes }, )); bytes })) } /// Applies the [algorithm from /// XEP-0390](https://xmpp.org/extensions/xep-0390.html#algorithm-input) on a /// [disco#info query element](../disco/struct.DiscoInfoResult.html). pub fn compute_disco(disco: &DiscoInfoResult) -> Result, Error> { let features_string = compute_features(&disco.features); let identities_string = compute_identities(&disco.identities); let extensions_string = compute_extensions(&disco.extensions)?; let mut final_string = vec![]; final_string.extend(features_string); final_string.extend(identities_string); final_string.extend(extensions_string); Ok(final_string) } /// Hashes the result of [compute_disco()] with one of the supported [hash /// algorithms](../hashes/enum.Algo.html). pub fn hash_ecaps2(data: &[u8], algo: Algo) -> Result { Ok(Hash { hash: match algo { Algo::Sha_256 => { let hash = Sha256::digest(data); hash.to_vec() } Algo::Sha_512 => { let hash = Sha512::digest(data); hash.to_vec() } Algo::Sha3_256 => { let hash = Sha3_256::digest(data); hash.to_vec() } Algo::Sha3_512 => { let hash = Sha3_512::digest(data); hash.to_vec() } Algo::Blake2b_256 => { let mut hasher = Blake2bVar::new(32).unwrap(); hasher.update(data); let mut vec = vec![0u8; 32]; hasher.finalize_variable(&mut vec).unwrap(); vec } Algo::Blake2b_512 => { let mut hasher = Blake2bVar::new(64).unwrap(); hasher.update(data); let mut vec = vec![0u8; 64]; hasher.finalize_variable(&mut vec).unwrap(); vec } Algo::Sha_1 => return Err(Error::Other("Disabled algorithm sha-1: unsafe.")), Algo::Unknown(_algo) => return Err(Error::Other("Unknown algorithm in ecaps2.")), }, algo, }) } /// Helper function to create the query for the disco#info corresponding to an /// ecaps2 hash. pub fn query_ecaps2(hash: Hash) -> DiscoInfoQuery { DiscoInfoQuery { node: Some(format!( "{}#{}.{}", ns::ECAPS2, String::from(hash.algo), Base64.encode(&hash.hash) )), } } #[cfg(test)] mod tests { use super::*; use minidom::Element; use xso::error::FromElementError; #[cfg(target_pointer_width = "32")] #[test] fn test_size() { assert_size!(ECaps2, 12); } #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(ECaps2, 24); } #[test] fn test_parse() { let elem: Element = "K1Njy3HZBThlo4moOD5gBGhn0U0oK7/CbfLlIUDi6o4=+sDTQqBmX6iG/X3zjt06fjZMBBqL/723knFIyRf0sg8=".parse().unwrap(); let ecaps2 = ECaps2::try_from(elem).unwrap(); assert_eq!(ecaps2.hashes.len(), 2); assert_eq!(ecaps2.hashes[0].algo, Algo::Sha_256); assert_eq!( ecaps2.hashes[0].hash, Base64 .decode("K1Njy3HZBThlo4moOD5gBGhn0U0oK7/CbfLlIUDi6o4=") .unwrap() ); assert_eq!(ecaps2.hashes[1].algo, Algo::Sha3_256); assert_eq!( ecaps2.hashes[1].hash, Base64 .decode("+sDTQqBmX6iG/X3zjt06fjZMBBqL/723knFIyRf0sg8=") .unwrap() ); } #[test] #[cfg_attr(feature = "disable-validation", should_panic = "Result::unwrap_err")] fn test_invalid_child() { let elem: Element = "K1Njy3HZBThlo4moOD5gBGhn0U0oK7/CbfLlIUDi6o4=+sDTQqBmX6iG/X3zjt06fjZMBBqL/723knFIyRf0sg8=".parse().unwrap(); let error = ECaps2::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!(message, "Unknown child in ECaps2 element."); } #[test] fn test_simple() { let elem: Element = "".parse().unwrap(); let disco = DiscoInfoResult::try_from(elem).unwrap(); let ecaps2 = compute_disco(&disco).unwrap(); assert_eq!(ecaps2.len(), 54); } #[test] fn test_xep_ex1() { let elem: Element = r#" "# .parse() .unwrap(); let expected = vec![ 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 98, 121, 116, 101, 115, 116, 114, 101, 97, 109, 115, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 99, 104, 97, 116, 115, 116, 97, 116, 101, 115, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 100, 105, 115, 99, 111, 35, 105, 110, 102, 111, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 100, 105, 115, 99, 111, 35, 105, 116, 101, 109, 115, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 105, 98, 98, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 114, 111, 115, 116, 101, 114, 120, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 115, 105, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 115, 105, 47, 112, 114, 111, 102, 105, 108, 101, 47, 102, 105, 108, 101, 45, 116, 114, 97, 110, 115, 102, 101, 114, 31, 106, 97, 98, 98, 101, 114, 58, 105, 113, 58, 108, 97, 115, 116, 31, 106, 97, 98, 98, 101, 114, 58, 105, 113, 58, 112, 114, 105, 118, 97, 99, 121, 31, 106, 97, 98, 98, 101, 114, 58, 105, 113, 58, 114, 111, 115, 116, 101, 114, 31, 106, 97, 98, 98, 101, 114, 58, 105, 113, 58, 116, 105, 109, 101, 31, 106, 97, 98, 98, 101, 114, 58, 105, 113, 58, 118, 101, 114, 115, 105, 111, 110, 31, 106, 97, 98, 98, 101, 114, 58, 120, 58, 111, 111, 98, 31, 117, 114, 110, 58, 120, 109, 112, 112, 58, 112, 105, 110, 103, 31, 117, 114, 110, 58, 120, 109, 112, 112, 58, 114, 101, 99, 101, 105, 112, 116, 115, 31, 117, 114, 110, 58, 120, 109, 112, 112, 58, 116, 105, 109, 101, 31, 28, 99, 108, 105, 101, 110, 116, 31, 109, 111, 98, 105, 108, 101, 31, 31, 66, 111, 109, 98, 117, 115, 77, 111, 100, 31, 30, 28, 28, ]; let disco = DiscoInfoResult::try_from(elem).unwrap(); let ecaps2 = compute_disco(&disco).unwrap(); assert_eq!(ecaps2.len(), 0x1d9); assert_eq!(ecaps2, expected); let sha_256 = hash_ecaps2(&ecaps2, Algo::Sha_256).unwrap(); assert_eq!( sha_256.hash, Base64 .decode("kzBZbkqJ3ADrj7v08reD1qcWUwNGHaidNUgD7nHpiw8=") .unwrap() ); let sha3_256 = hash_ecaps2(&ecaps2, Algo::Sha3_256).unwrap(); assert_eq!( sha3_256.hash, Base64 .decode("79mdYAfU9rEdTOcWDO7UEAt6E56SUzk/g6TnqUeuD9Q=") .unwrap() ); } #[test] fn test_xep_ex2() { let elem: Element = r#" urn:xmpp:dataforms:softwareinfo Tkabber 0.11.1-svn-20111216-mod (Tcl/Tk 8.6b2) Windows XP "# .parse() .unwrap(); let expected = vec![ 103, 97, 109, 101, 115, 58, 98, 111, 97, 114, 100, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 97, 99, 116, 105, 118, 105, 116, 121, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 97, 99, 116, 105, 118, 105, 116, 121, 43, 110, 111, 116, 105, 102, 121, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 98, 121, 116, 101, 115, 116, 114, 101, 97, 109, 115, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 99, 104, 97, 116, 115, 116, 97, 116, 101, 115, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 99, 111, 109, 109, 97, 110, 100, 115, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 100, 105, 115, 99, 111, 35, 105, 110, 102, 111, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 100, 105, 115, 99, 111, 35, 105, 116, 101, 109, 115, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 101, 118, 105, 108, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 102, 101, 97, 116, 117, 114, 101, 45, 110, 101, 103, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 103, 101, 111, 108, 111, 99, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 103, 101, 111, 108, 111, 99, 43, 110, 111, 116, 105, 102, 121, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 105, 98, 98, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 105, 113, 105, 98, 98, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 109, 111, 111, 100, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 109, 111, 111, 100, 43, 110, 111, 116, 105, 102, 121, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 114, 111, 115, 116, 101, 114, 120, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 115, 105, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 115, 105, 47, 112, 114, 111, 102, 105, 108, 101, 47, 102, 105, 108, 101, 45, 116, 114, 97, 110, 115, 102, 101, 114, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 116, 117, 110, 101, 31, 104, 116, 116, 112, 58, 47, 47, 119, 119, 119, 46, 102, 97, 99, 101, 98, 111, 111, 107, 46, 99, 111, 109, 47, 120, 109, 112, 112, 47, 109, 101, 115, 115, 97, 103, 101, 115, 31, 104, 116, 116, 112, 58, 47, 47, 119, 119, 119, 46, 120, 109, 112, 112, 46, 111, 114, 103, 47, 101, 120, 116, 101, 110, 115, 105, 111, 110, 115, 47, 120, 101, 112, 45, 48, 48, 56, 52, 46, 104, 116, 109, 108, 35, 110, 115, 45, 109, 101, 116, 97, 100, 97, 116, 97, 43, 110, 111, 116, 105, 102, 121, 31, 106, 97, 98, 98, 101, 114, 58, 105, 113, 58, 97, 118, 97, 116, 97, 114, 31, 106, 97, 98, 98, 101, 114, 58, 105, 113, 58, 98, 114, 111, 119, 115, 101, 31, 106, 97, 98, 98, 101, 114, 58, 105, 113, 58, 100, 116, 99, 112, 31, 106, 97, 98, 98, 101, 114, 58, 105, 113, 58, 102, 105, 108, 101, 120, 102, 101, 114, 31, 106, 97, 98, 98, 101, 114, 58, 105, 113, 58, 105, 98, 98, 31, 106, 97, 98, 98, 101, 114, 58, 105, 113, 58, 105, 110, 98, 97, 110, 100, 31, 106, 97, 98, 98, 101, 114, 58, 105, 113, 58, 106, 105, 100, 108, 105, 110, 107, 31, 106, 97, 98, 98, 101, 114, 58, 105, 113, 58, 108, 97, 115, 116, 31, 106, 97, 98, 98, 101, 114, 58, 105, 113, 58, 111, 111, 98, 31, 106, 97, 98, 98, 101, 114, 58, 105, 113, 58, 112, 114, 105, 118, 97, 99, 121, 31, 106, 97, 98, 98, 101, 114, 58, 105, 113, 58, 114, 111, 115, 116, 101, 114, 31, 106, 97, 98, 98, 101, 114, 58, 105, 113, 58, 116, 105, 109, 101, 31, 106, 97, 98, 98, 101, 114, 58, 105, 113, 58, 118, 101, 114, 115, 105, 111, 110, 31, 106, 97, 98, 98, 101, 114, 58, 120, 58, 100, 97, 116, 97, 31, 106, 97, 98, 98, 101, 114, 58, 120, 58, 101, 118, 101, 110, 116, 31, 106, 97, 98, 98, 101, 114, 58, 120, 58, 111, 111, 98, 31, 117, 114, 110, 58, 120, 109, 112, 112, 58, 97, 118, 97, 116, 97, 114, 58, 109, 101, 116, 97, 100, 97, 116, 97, 43, 110, 111, 116, 105, 102, 121, 31, 117, 114, 110, 58, 120, 109, 112, 112, 58, 112, 105, 110, 103, 31, 117, 114, 110, 58, 120, 109, 112, 112, 58, 114, 101, 99, 101, 105, 112, 116, 115, 31, 117, 114, 110, 58, 120, 109, 112, 112, 58, 116, 105, 109, 101, 31, 28, 99, 108, 105, 101, 110, 116, 31, 112, 99, 31, 101, 110, 31, 84, 107, 97, 98, 98, 101, 114, 31, 30, 99, 108, 105, 101, 110, 116, 31, 112, 99, 31, 114, 117, 31, 208, 162, 208, 186, 208, 176, 208, 177, 208, 177, 208, 181, 209, 128, 31, 30, 28, 70, 79, 82, 77, 95, 84, 89, 80, 69, 31, 117, 114, 110, 58, 120, 109, 112, 112, 58, 100, 97, 116, 97, 102, 111, 114, 109, 115, 58, 115, 111, 102, 116, 119, 97, 114, 101, 105, 110, 102, 111, 31, 30, 111, 115, 31, 87, 105, 110, 100, 111, 119, 115, 31, 30, 111, 115, 95, 118, 101, 114, 115, 105, 111, 110, 31, 88, 80, 31, 30, 115, 111, 102, 116, 119, 97, 114, 101, 31, 84, 107, 97, 98, 98, 101, 114, 31, 30, 115, 111, 102, 116, 119, 97, 114, 101, 95, 118, 101, 114, 115, 105, 111, 110, 31, 48, 46, 49, 49, 46, 49, 45, 115, 118, 110, 45, 50, 48, 49, 49, 49, 50, 49, 54, 45, 109, 111, 100, 32, 40, 84, 99, 108, 47, 84, 107, 32, 56, 46, 54, 98, 50, 41, 31, 30, 29, 28, ]; let disco = DiscoInfoResult::try_from(elem).unwrap(); let ecaps2 = compute_disco(&disco).unwrap(); assert_eq!(ecaps2.len(), 0x543); assert_eq!(ecaps2, expected); let sha_256 = hash_ecaps2(&ecaps2, Algo::Sha_256).unwrap(); assert_eq!( sha_256.hash, Base64 .decode("u79ZroNJbdSWhdSp311mddz44oHHPsEBntQ5b1jqBSY=") .unwrap() ); let sha3_256 = hash_ecaps2(&ecaps2, Algo::Sha3_256).unwrap(); assert_eq!( sha3_256.hash, Base64 .decode("XpUJzLAc93258sMECZ3FJpebkzuyNXDzRNwQog8eycg=") .unwrap() ); } #[test] fn test_blake2b_512() { let hash = hash_ecaps2("abc".as_bytes(), Algo::Blake2b_512).unwrap(); let known_hash: Vec = vec![ 0xBA, 0x80, 0xA5, 0x3F, 0x98, 0x1C, 0x4D, 0x0D, 0x6A, 0x27, 0x97, 0xB6, 0x9F, 0x12, 0xF6, 0xE9, 0x4C, 0x21, 0x2F, 0x14, 0x68, 0x5A, 0xC4, 0xB7, 0x4B, 0x12, 0xBB, 0x6F, 0xDB, 0xFF, 0xA2, 0xD1, 0x7D, 0x87, 0xC5, 0x39, 0x2A, 0xAB, 0x79, 0x2D, 0xC2, 0x52, 0xD5, 0xDE, 0x45, 0x33, 0xCC, 0x95, 0x18, 0xD3, 0x8A, 0xA8, 0xDB, 0xF1, 0x92, 0x5A, 0xB9, 0x23, 0x86, 0xED, 0xD4, 0x00, 0x99, 0x23, ]; assert_eq!(hash.hash, known_hash); } } xmpp-parsers-0.22.0/src/eme.rs000064400000000000000000000065671046102023000142670ustar 00000000000000// Copyright (c) 2017 Emmanuel Gil Peyrot // // 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 xso::{AsXml, FromXml}; use crate::message::MessagePayload; use crate::ns; /// Structure representing an `` element. #[derive(FromXml, AsXml, Debug, Clone, PartialEq)] #[xml(namespace = ns::EME, name = "encryption")] pub struct ExplicitMessageEncryption { /// Namespace of the encryption scheme used. #[xml(attribute)] pub namespace: String, /// User-friendly name for the encryption scheme, should be `None` for OTR, /// legacy OpenPGP and OX. #[xml(attribute(default))] pub name: Option, } impl MessagePayload for ExplicitMessageEncryption {} #[cfg(test)] mod tests { use super::*; use minidom::Element; use xso::error::{Error, FromElementError}; #[cfg(target_pointer_width = "32")] #[test] fn test_size() { assert_size!(ExplicitMessageEncryption, 24); } #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(ExplicitMessageEncryption, 48); } #[test] fn test_simple() { let elem: Element = "" .parse() .unwrap(); let encryption = ExplicitMessageEncryption::try_from(elem).unwrap(); assert_eq!(encryption.namespace, "urn:xmpp:otr:0"); assert_eq!(encryption.name, None); let elem: Element = "".parse().unwrap(); let encryption = ExplicitMessageEncryption::try_from(elem).unwrap(); assert_eq!(encryption.namespace, "some.unknown.mechanism"); assert_eq!(encryption.name, Some(String::from("SuperMechanism"))); } #[test] fn test_unknown() { let elem: Element = "" .parse() .unwrap(); let error = ExplicitMessageEncryption::try_from(elem.clone()).unwrap_err(); let returned_elem = match error { FromElementError::Mismatch(elem) => elem, _ => panic!(), }; assert_eq!(elem, returned_elem); } #[test] #[cfg_attr(feature = "disable-validation", should_panic = "Result::unwrap_err")] fn test_invalid_child() { let elem: Element = "" .parse() .unwrap(); let error = ExplicitMessageEncryption::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!( message, "Unknown child in ExplicitMessageEncryption element." ); } #[test] fn test_serialise() { let elem: Element = "" .parse() .unwrap(); let eme = ExplicitMessageEncryption { namespace: String::from("coucou"), name: None, }; let elem2 = eme.into(); assert_eq!(elem, elem2); } } xmpp-parsers-0.22.0/src/extdisco.rs000064400000000000000000000154001046102023000153250ustar 00000000000000// Copyright (c) 2021 Emmanuel Gil Peyrot // // 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 xso::{AsXml, FromXml}; use crate::data_forms::DataForm; use crate::date::DateTime; use crate::iq::{IqGetPayload, IqResultPayload, IqSetPayload}; use crate::ns; generate_attribute!( /// When sending a push update, the action value indicates if the service is being added or /// deleted from the set of known services (or simply being modified). Action, "action", { /// The service is being added from the set of known services. Add => "add", /// The service is being removed from the set of known services. Remove => "remove", /// The service is being modified. Modify => "modify", }, Default = Add ); generate_attribute!( /// The underlying transport protocol to be used when communicating with the service. Transport, "transport", { /// Use TCP as a transport protocol. Tcp => "tcp", /// Use UDP as a transport protocol. Udp => "udp", } ); generate_attribute!( /// The service type as registered with the XMPP Registrar. Type, "type", { /// A server that provides Session Traversal Utilities for NAT (STUN). Stun => "stun", /// A server that provides Traversal Using Relays around NAT (TURN). Turn => "turn", } ); /// Structure representing a `` element. #[derive(FromXml, AsXml, Debug, PartialEq, Clone)] #[xml(namespace = ns::EXT_DISCO, name = "service")] pub struct Service { /// When sending a push update, the action value indicates if the service is being added or /// deleted from the set of known services (or simply being modified). #[xml(attribute(default))] action: Action, /// A timestamp indicating when the provided username and password credentials will expire. #[xml(attribute(default))] expires: Option, /// Either a fully qualified domain name (FQDN) or an IP address (IPv4 or IPv6). #[xml(attribute)] host: String, /// A friendly (human-readable) name or label for the service. #[xml(attribute(default))] name: Option, /// A service- or server-generated password for use at the service. #[xml(attribute(default))] password: Option, /// The communications port to be used at the host. #[xml(attribute(default))] port: Option, /// A boolean value indicating that username and password credentials are required and will /// need to be requested if not already provided. #[xml(attribute(default))] restricted: bool, /// The underlying transport protocol to be used when communicating with the service (typically /// either TCP or UDP). #[xml(attribute(default))] transport: Option, /// The service type as registered with the XMPP Registrar. #[xml(attribute = "type")] type_: Type, /// A service- or server-generated username for use at the service. #[xml(attribute(default))] username: Option, /// Extended information #[xml(child(n = ..))] ext_info: Vec, } impl IqGetPayload for Service {} /// Structure representing a `` element. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::EXT_DISCO, name = "services")] pub struct ServicesQuery { /// The type of service to filter for. #[xml(attribute(default, name = "type"))] pub type_: Option, } impl IqGetPayload for ServicesQuery {} /// Structure representing a `` element. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::EXT_DISCO, name = "services")] pub struct ServicesResult { /// The service type which was requested. #[xml(attribute(name = "type", default))] pub type_: Option, /// List of services. #[xml(child(n = ..))] pub services: Vec, } impl IqResultPayload for ServicesResult {} impl IqSetPayload for ServicesResult {} /// Structure representing a `` element. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::EXT_DISCO, name = "credentials")] pub struct Credentials { /// List of services. #[xml(child(n = ..))] pub services: Vec, } impl IqGetPayload for Credentials {} impl IqResultPayload for Credentials {} #[cfg(test)] mod tests { use super::*; use crate::ns; use minidom::Element; #[cfg(target_pointer_width = "32")] #[test] fn test_size() { assert_size!(Action, 1); assert_size!(Transport, 1); assert_size!(Type, 1); assert_size!(Service, 84); assert_size!(ServicesQuery, 1); assert_size!(ServicesResult, 16); assert_size!(Credentials, 12); } #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(Action, 1); assert_size!(Transport, 1); assert_size!(Type, 1); assert_size!(Service, 144); assert_size!(ServicesQuery, 1); assert_size!(ServicesResult, 32); assert_size!(Credentials, 24); } #[test] fn test_simple() { let elem: Element = "".parse().unwrap(); let service = Service::try_from(elem).unwrap(); assert_eq!(service.action, Action::Add); assert!(service.expires.is_none()); assert_eq!(service.host, "stun.shakespeare.lit"); assert!(service.name.is_none()); assert!(service.password.is_none()); assert_eq!(service.port.unwrap(), 9998); assert_eq!(service.restricted, false); assert_eq!(service.transport.unwrap(), Transport::Udp); assert_eq!(service.type_, Type::Stun); assert!(service.username.is_none()); assert!(service.ext_info.is_empty()); } #[test] fn test_service_query() { let query = ServicesQuery { type_: None }; let elem = Element::from(query); assert!(elem.is("services", ns::EXT_DISCO)); assert_eq!(elem.attrs().into_iter().next(), None); assert_eq!(elem.nodes().next(), None); } #[test] fn test_service_result() { let elem: Element = "".parse().unwrap(); let services = ServicesResult::try_from(elem).unwrap(); assert_eq!(services.type_, Some(Type::Stun)); assert_eq!(services.services.len(), 1); } } xmpp-parsers-0.22.0/src/fast.rs000064400000000000000000000076721046102023000144540ustar 00000000000000// Copyright (c) 2024 Emmanuel Gil Peyrot // // 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 xso::{AsXml, FromXml}; use crate::date::DateTime; use crate::ns; generate_elem_id!( /// A `` element, describing one mechanism allowed by the server to authenticate. Mechanism, "mechanism", FAST ); /// This is the `` element sent by the server as a SASL2 inline feature. #[derive(FromXml, AsXml, Debug, Clone, PartialEq)] #[xml(namespace = ns::FAST, name = "fast")] pub struct FastQuery { /// Whether TLS zero-roundtrip is possible. #[xml(attribute(default, name = "tls-0rtt"))] pub tls_0rtt: bool, /// A list of `` elements, listing all server allowed mechanisms. #[xml(child(n = ..))] pub mechanisms: Vec, } /// This is the `` element the client MUST include within its SASL2 authentication request. #[derive(FromXml, AsXml, Debug, Clone, PartialEq)] #[xml(namespace = ns::FAST, name = "fast")] pub struct FastResponse { /// Servers MUST reject any authentication requests received via TLS 0-RTT payloads that do not /// include a 'count' attribute, or where the count is less than or equal to a count that has /// already been processed for this token. This protects against replay attacks that 0-RTT is /// susceptible to. #[xml(attribute)] pub count: u32, /// If true and the client has successfully authenticated, the server MUST invalidate the /// token. #[xml(attribute(default))] pub invalidate: bool, } /// This is the `` element sent by the client in the SASL2 authenticate step. #[derive(FromXml, AsXml, Debug, Clone, PartialEq)] #[xml(namespace = ns::FAST, name = "request-token")] pub struct RequestToken { /// This element MUST contain a 'mechanism' attribute, the value of which MUST be one of the /// FAST mechanisms advertised by the server. #[xml(attribute)] pub mechanism: String, } /// This is the `` element sent by the server on successful SASL2 authentication containing /// a `` element. #[derive(FromXml, AsXml, Debug, Clone, PartialEq)] #[xml(namespace = ns::FAST, name = "token")] pub struct Token { /// The secret token to be used for subsequent authentications, as generated by the server. #[xml(attribute)] pub token: String, /// The timestamp at which the token will expire. #[xml(attribute)] pub expiry: DateTime, } #[cfg(test)] mod tests { use super::*; use core::str::FromStr; use minidom::Element; #[test] fn test_simple() { let elem: Element = "FOO-BAR" .parse() .unwrap(); let request = FastQuery::try_from(elem).unwrap(); assert_eq!(request.tls_0rtt, false); assert_eq!(request.mechanisms, [Mechanism(String::from("FOO-BAR"))]); let elem: Element = "" .parse() .unwrap(); let response = FastResponse::try_from(elem).unwrap(); assert_eq!(response.count, 123); assert_eq!(response.invalidate, false); let elem: Element = "" .parse() .unwrap(); let request_token = RequestToken::try_from(elem).unwrap(); assert_eq!(request_token.mechanism, "FOO-BAR"); let elem: Element = "" .parse() .unwrap(); let token = Token::try_from(elem).unwrap(); assert_eq!(token.token, "ABCD"); assert_eq!( token.expiry, DateTime::from_str("2024-06-30T17:13:57+02:00").unwrap() ); } } xmpp-parsers-0.22.0/src/forwarding.rs000064400000000000000000000123301046102023000156440ustar 00000000000000// Copyright (c) 2017 Emmanuel Gil Peyrot // // 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 xso::{AsXml, FromXml}; use crate::delay::Delay; use crate::message::Message; use crate::ns; /// Contains a forwarded stanza, either standalone or part of another /// extension (such as carbons). #[derive(FromXml, AsXml, Debug, Clone, PartialEq)] #[xml(namespace = ns::FORWARD, name = "forwarded")] pub struct Forwarded { /// When the stanza originally got sent. #[xml(child(default))] pub delay: Option, /// The stanza being forwarded. // The schema says that we should allow either a Message, Presence or Iq, in either // jabber:client or jabber:server, but in the wild so far we’ve only seen Message being // transmitted, so let’s hardcode that for now. The schema also makes it optional, but so far // it’s always present (or this wrapper is useless). #[xml(child)] pub message: Message, } #[cfg(test)] mod tests { use super::*; use minidom::Element; use xso::error::{Error, FromElementError}; #[cfg(target_pointer_width = "32")] #[test] fn test_size() { assert_size!(Forwarded, 152); } #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(Forwarded, 288); } #[test] fn test_simple() { let elem: Element = "" .parse() .unwrap(); Forwarded::try_from(elem).unwrap(); } #[test] #[cfg_attr(feature = "disable-validation", should_panic = "Result::unwrap_err")] fn test_invalid_child() { let elem: Element = "" .parse() .unwrap(); let error = Forwarded::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!(message, "Unknown child in Forwarded element."); } #[test] fn test_serialise() { let elem: Element = "".parse().unwrap(); let forwarded = Forwarded { delay: None, message: Message::new(None), }; let elem2 = forwarded.into(); assert_eq!(elem, elem2); } #[test] fn test_serialize_with_delay_and_stanza() { let reference: Element = "" .parse() .unwrap(); let elem: Element = "" .parse() .unwrap(); let message = Message::try_from(elem).unwrap(); let elem: Element = "" .parse() .unwrap(); let delay = Delay::try_from(elem).unwrap(); let forwarded = Forwarded { delay: Some(delay), message, }; let serialized: Element = forwarded.into(); assert_eq!(serialized, reference); } #[test] fn test_invalid_duplicate_delay() { let elem: Element = "" .parse() .unwrap(); let error = Forwarded::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!( message, "Forwarded element must not have more than one child in field 'delay'." ); } #[test] fn test_invalid_duplicate_message() { let elem: Element = "" .parse() .unwrap(); let error = Forwarded::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!( message, "Forwarded element must not have more than one child in field 'message'." ); } } xmpp-parsers-0.22.0/src/hashes.rs000064400000000000000000000225431046102023000147640ustar 00000000000000// Copyright (c) 2017 Emmanuel Gil Peyrot // // 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::{ num::ParseIntError, ops::{Deref, DerefMut}, str::FromStr, }; use xso::{error::Error, text::Base64, AsXml, AsXmlText, FromXml, FromXmlText}; use base64::{engine::general_purpose::STANDARD as Base64Engine, Engine}; use minidom::IntoAttributeValue; use crate::ns; /// List of the algorithms we support, or Unknown. #[allow(non_camel_case_types)] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum Algo { /// The Secure Hash Algorithm 1, with known vulnerabilities, do not use it. /// /// See Sha_1, /// The Secure Hash Algorithm 2, in its 256-bit version. /// /// See Sha_256, /// The Secure Hash Algorithm 2, in its 512-bit version. /// /// See Sha_512, /// The Secure Hash Algorithm 3, based on Keccak, in its 256-bit version. /// /// See Sha3_256, /// The Secure Hash Algorithm 3, based on Keccak, in its 512-bit version. /// /// See Sha3_512, /// The BLAKE2 hash algorithm, for a 256-bit output. /// /// See Blake2b_256, /// The BLAKE2 hash algorithm, for a 512-bit output. /// /// See Blake2b_512, /// An unknown hash not in this list, you can probably reject it. Unknown(String), } impl FromStr for Algo { type Err = Error; fn from_str(s: &str) -> Result { Ok(match s { "" => return Err(Error::Other("'algo' argument can’t be empty.")), "sha-1" => Algo::Sha_1, "sha-256" => Algo::Sha_256, "sha-512" => Algo::Sha_512, "sha3-256" => Algo::Sha3_256, "sha3-512" => Algo::Sha3_512, "blake2b-256" => Algo::Blake2b_256, "blake2b-512" => Algo::Blake2b_512, value => Algo::Unknown(value.to_owned()), }) } } impl From for String { fn from(algo: Algo) -> String { String::from(match algo { Algo::Sha_1 => "sha-1", Algo::Sha_256 => "sha-256", Algo::Sha_512 => "sha-512", Algo::Sha3_256 => "sha3-256", Algo::Sha3_512 => "sha3-512", Algo::Blake2b_256 => "blake2b-256", Algo::Blake2b_512 => "blake2b-512", Algo::Unknown(text) => return text, }) } } impl FromXmlText for Algo { fn from_xml_text(value: String) -> Result { value.parse().map_err(Error::text_parse_error) } } impl AsXmlText for Algo { fn as_xml_text(&self) -> Result, Error> { Ok(Cow::Borrowed(match self { Algo::Sha_1 => "sha-1", Algo::Sha_256 => "sha-256", Algo::Sha_512 => "sha-512", Algo::Sha3_256 => "sha3-256", Algo::Sha3_512 => "sha3-512", Algo::Blake2b_256 => "blake2b-256", Algo::Blake2b_512 => "blake2b-512", Algo::Unknown(text) => text.as_str(), })) } } impl IntoAttributeValue for Algo { fn into_attribute_value(self) -> Option { Some(String::from(self)) } } /// This element represents a hash of some data, defined by the hash /// algorithm used and the computed value. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::HASHES, name = "hash")] pub struct Hash { /// The algorithm used to create this hash. #[xml(attribute)] pub algo: Algo, /// The hash value, as a vector of bytes. #[xml(text = Base64)] pub hash: Vec, } impl Hash { /// Creates a [struct@Hash] element with the given algo and data. pub fn new(algo: Algo, hash: Vec) -> Hash { Hash { algo, hash } } /// Like [new](#method.new) but takes base64-encoded data before decoding /// it. pub fn from_base64(algo: Algo, hash: &str) -> Result { Ok(Hash::new( algo, Base64Engine.decode(hash).map_err(Error::text_parse_error)?, )) } /// Like [new](#method.new) but takes hex-encoded data before decoding it. pub fn from_hex(algo: Algo, hex: &str) -> Result { let mut bytes = vec![]; for i in 0..hex.len() / 2 { let byte = u8::from_str_radix(&hex[2 * i..2 * i + 2], 16)?; bytes.push(byte); } Ok(Hash::new(algo, bytes)) } /// Like [new](#method.new) but takes hex-encoded data before decoding it. pub fn from_colon_separated_hex(algo: Algo, hex: &str) -> Result { let mut bytes = vec![]; for i in 0..(1 + hex.len()) / 3 { let byte = u8::from_str_radix(&hex[3 * i..3 * i + 2], 16)?; if 3 * i + 2 < hex.len() { assert_eq!(&hex[3 * i + 2..3 * i + 3], ":"); } bytes.push(byte); } Ok(Hash::new(algo, bytes)) } /// Formats this hash into base64. pub fn to_base64(&self) -> String { Base64Engine.encode(&self.hash[..]) } /// Formats this hash into hexadecimal. pub fn to_hex(&self) -> String { self.hash .iter() .map(|byte| format!("{:02x}", byte)) .collect::>() .join("") } /// Formats this hash into colon-separated hexadecimal. pub fn to_colon_separated_hex(&self) -> String { self.hash .iter() .map(|byte| format!("{:02x}", byte)) .collect::>() .join(":") } } /// Helper for parsing and serialising a SHA-1 attribute. #[derive(Debug, Clone, PartialEq)] pub struct Sha1HexAttribute(Hash); impl FromStr for Sha1HexAttribute { type Err = ParseIntError; fn from_str(hex: &str) -> Result { let hash = Hash::from_hex(Algo::Sha_1, hex)?; Ok(Sha1HexAttribute(hash)) } } impl FromXmlText for Sha1HexAttribute { fn from_xml_text(s: String) -> Result { Self::from_str(&s).map_err(xso::error::Error::text_parse_error) } } impl AsXmlText for Sha1HexAttribute { fn as_xml_text(&self) -> Result, xso::error::Error> { Ok(Cow::Owned(self.to_hex())) } } impl IntoAttributeValue for Sha1HexAttribute { fn into_attribute_value(self) -> Option { Some(self.to_hex()) } } impl DerefMut for Sha1HexAttribute { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } impl Deref for Sha1HexAttribute { type Target = Hash; fn deref(&self) -> &Self::Target { &self.0 } } #[cfg(test)] mod tests { use super::*; use minidom::Element; use xso::error::FromElementError; #[cfg(target_pointer_width = "32")] #[test] fn test_size() { assert_size!(Algo, 12); assert_size!(Hash, 24); } #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(Algo, 24); assert_size!(Hash, 48); } #[test] fn test_simple() { let elem: Element = "2XarmwTlNxDAMkvymloX3S5+VbylNrJt/l5QyPa+YoU=".parse().unwrap(); let hash = Hash::try_from(elem).unwrap(); assert_eq!(hash.algo, Algo::Sha_256); assert_eq!( hash.hash, Base64Engine .decode("2XarmwTlNxDAMkvymloX3S5+VbylNrJt/l5QyPa+YoU=") .unwrap() ); } #[test] fn value_serialisation() { let elem: Element = "2XarmwTlNxDAMkvymloX3S5+VbylNrJt/l5QyPa+YoU=".parse().unwrap(); let hash = Hash::try_from(elem).unwrap(); assert_eq!( hash.to_base64(), "2XarmwTlNxDAMkvymloX3S5+VbylNrJt/l5QyPa+YoU=" ); assert_eq!( hash.to_hex(), "d976ab9b04e53710c0324bf29a5a17dd2e7e55bca536b26dfe5e50c8f6be6285" ); assert_eq!(hash.to_colon_separated_hex(), "d9:76:ab:9b:04:e5:37:10:c0:32:4b:f2:9a:5a:17:dd:2e:7e:55:bc:a5:36:b2:6d:fe:5e:50:c8:f6:be:62:85"); } #[test] fn test_unknown() { let elem: Element = "" .parse() .unwrap(); let error = Hash::try_from(elem.clone()).unwrap_err(); let returned_elem = match error { FromElementError::Mismatch(elem) => elem, _ => panic!(), }; assert_eq!(elem, returned_elem); } #[test] #[cfg_attr(feature = "disable-validation", should_panic = "Result::unwrap_err")] fn test_invalid_child() { let elem: Element = "" .parse() .unwrap(); let error = Hash::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!(message, "Unknown child in Hash element."); } } xmpp-parsers-0.22.0/src/http_upload.rs000064400000000000000000000144531046102023000160350ustar 00000000000000// Copyright (c) 2021 Maxime “pep” Buquet // // 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 xso::{error::Error, AsXml, AsXmlText, FromXml, FromXmlText}; use crate::iq::{IqGetPayload, IqResultPayload}; use crate::ns; use alloc::borrow::Cow; /// Requesting a slot #[derive(FromXml, AsXml, Debug, Clone, PartialEq)] #[xml(namespace = ns::HTTP_UPLOAD, name = "request")] pub struct SlotRequest { /// The filename to be uploaded. #[xml(attribute)] pub filename: String, /// Size of the file to be uploaded. #[xml(attribute)] pub size: u64, /// Content-Type of the file. #[xml(attribute(name = "content-type"))] pub content_type: Option, } impl IqGetPayload for SlotRequest {} /// All three possible header names. #[derive(Debug, Clone, PartialEq)] pub enum HeaderName { /// Authorization header Authorization, /// Cookie header Cookie, /// Expires header Expires, } impl HeaderName { /// Returns the string version of this enum value. pub fn as_str(&self) -> &'static str { match self { HeaderName::Authorization => "Authorization", HeaderName::Cookie => "Cookie", HeaderName::Expires => "Expires", } } } impl FromXmlText for HeaderName { fn from_xml_text(mut s: String) -> Result { s.make_ascii_lowercase(); Ok(match s.as_str() { "authorization" => HeaderName::Authorization, "cookie" => HeaderName::Cookie, "expires" => HeaderName::Expires, _ => { return Err(Error::Other( "Header name must be either 'Authorization', 'Cookie', or 'Expires'.", )) } }) } } impl AsXmlText for HeaderName { fn as_xml_text(&self) -> Result, Error> { Ok(Cow::Borrowed(self.as_str())) } } /// Slot header #[derive(FromXml, AsXml, Debug, Clone, PartialEq)] #[xml(namespace = ns::HTTP_UPLOAD, name = "header")] pub struct Header { /// Name of the header #[xml(attribute)] pub name: HeaderName, /// Value of the header #[xml(text)] pub value: String, } /// Put URL #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::HTTP_UPLOAD, name = "put")] pub struct Put { /// URL #[xml(attribute)] pub url: String, /// Header list #[xml(child(n = ..))] pub headers: Vec
, } /// Get URL #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::HTTP_UPLOAD, name = "get")] pub struct Get { /// URL #[xml(attribute)] pub url: String, } /// Requesting a slot #[derive(FromXml, AsXml, Debug, Clone, PartialEq)] #[xml(namespace = ns::HTTP_UPLOAD, name = "slot")] pub struct SlotResult { /// Put URL and headers #[xml(child)] pub put: Put, /// Get URL #[xml(child)] pub get: Get, } impl IqResultPayload for SlotResult {} #[cfg(test)] mod tests { use super::*; use minidom::Element; #[cfg(target_pointer_width = "32")] #[test] fn test_size() { assert_size!(SlotRequest, 32); assert_size!(HeaderName, 1); assert_size!(Header, 16); assert_size!(Put, 24); assert_size!(Get, 12); assert_size!(SlotResult, 36); } #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(SlotRequest, 56); assert_size!(HeaderName, 1); assert_size!(Header, 32); assert_size!(Put, 48); assert_size!(Get, 24); assert_size!(SlotResult, 72); } #[test] fn test_slot_request() { let elem: Element = "" .parse() .unwrap(); let slot = SlotRequest::try_from(elem).unwrap(); assert_eq!(slot.filename, String::from("très cool.jpg")); assert_eq!(slot.size, 23456); assert_eq!(slot.content_type, Some(String::from("image/jpeg"))); } #[test] fn test_slot_result() { let elem: Element = "
Basic Base64String==
foo=bar; user=romeo
" .parse() .unwrap(); let slot = SlotResult::try_from(elem).unwrap(); assert_eq!(slot.put.url, String::from("https://upload.montague.tld/4a771ac1-f0b2-4a4a-9700-f2a26fa2bb67/tr%C3%A8s%20cool.jpg")); assert_eq!( slot.put.headers[0], Header { name: HeaderName::Authorization, value: String::from("Basic Base64String==") } ); assert_eq!( slot.put.headers[1], Header { name: HeaderName::Cookie, value: String::from("foo=bar; user=romeo") } ); assert_eq!(slot.get.url, String::from("https://download.montague.tld/4a771ac1-f0b2-4a4a-9700-f2a26fa2bb67/tr%C3%A8s%20cool.jpg")); } #[test] fn test_result_no_header() { let elem: Element = " " .parse() .unwrap(); let slot = SlotResult::try_from(elem).unwrap(); assert_eq!(slot.put.url, String::from("https://URL")); assert_eq!(slot.put.headers.len(), 0); assert_eq!(slot.get.url, String::from("https://URL")); } #[test] fn test_result_bad_header() { let elem: Element = "
EvilValue
" .parse() .unwrap(); SlotResult::try_from(elem).unwrap_err(); } } xmpp-parsers-0.22.0/src/ibb.rs000064400000000000000000000133141046102023000142410ustar 00000000000000// Copyright (c) 2017 Emmanuel Gil Peyrot // // 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 xso::{text::Base64, AsXml, FromXml}; use crate::iq::IqSetPayload; use crate::ns; generate_id!( /// An identifier matching a stream. StreamId ); generate_attribute!( /// Which stanza type to use to exchange data. Stanza, "stanza", { /// `` gives a feedback on whether the chunk has been received or not, /// which is useful in the case the recipient might not receive them in a /// timely manner, or to do your own throttling based on the results. Iq => "iq", /// `` can be faster, since it doesn’t require any feedback, but in /// practice it will be throttled by the servers on the way. Message => "message", }, Default = Iq); /// Starts an In-Band Bytestream session with the given parameters. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::IBB, name = "open")] pub struct Open { /// Maximum size in bytes for each chunk. #[xml(attribute(name = "block-size"))] pub block_size: u16, /// The identifier to be used to create a stream. #[xml(attribute)] pub sid: StreamId, /// Which stanza type to use to exchange data. #[xml(attribute(default))] pub stanza: Stanza, } impl IqSetPayload for Open {} /// Exchange a chunk of data in an open stream. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::IBB, name = "data")] pub struct Data { /// Sequence number of this chunk, must wraparound after 65535. #[xml(attribute)] pub seq: u16, /// The identifier of the stream on which data is being exchanged. #[xml(attribute)] pub sid: StreamId, /// Vector of bytes to be exchanged. #[xml(text = Base64)] pub data: Vec, } impl IqSetPayload for Data {} /// Close an open stream. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::IBB, name = "close")] pub struct Close { /// The identifier of the stream to be closed. #[xml(attribute)] pub sid: StreamId, } impl IqSetPayload for Close {} #[cfg(test)] mod tests { use super::*; use minidom::Element; use xso::error::{Error, FromElementError}; #[cfg(target_pointer_width = "32")] #[test] fn test_size() { assert_size!(StreamId, 12); assert_size!(Stanza, 1); assert_size!(Open, 16); assert_size!(Data, 28); assert_size!(Close, 12); } #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(StreamId, 24); assert_size!(Stanza, 1); assert_size!(Open, 32); assert_size!(Data, 56); assert_size!(Close, 24); } #[test] fn test_simple() { let sid = StreamId(String::from("coucou")); let elem: Element = "" .parse() .unwrap(); let open = Open::try_from(elem).unwrap(); assert_eq!(open.block_size, 3); assert_eq!(open.sid, sid); assert_eq!(open.stanza, Stanza::Iq); let elem: Element = "AAAA" .parse() .unwrap(); let data = Data::try_from(elem).unwrap(); assert_eq!(data.seq, 0); assert_eq!(data.sid, sid); assert_eq!(data.data, vec!(0, 0, 0)); let elem: Element = "" .parse() .unwrap(); let close = Close::try_from(elem).unwrap(); assert_eq!(close.sid, sid); } #[test] fn test_invalid() { let elem: Element = "" .parse() .unwrap(); let error = Open::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!( message, "Required attribute field 'block_size' on Open element missing." ); let elem: Element = "" .parse() .unwrap(); let error = Open::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::TextParseError(error)) if error.is::() => { error } _ => panic!(), }; assert_eq!(message.to_string(), "invalid digit found in string"); let elem: Element = "" .parse() .unwrap(); let error = Open::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(error)) => error, _ => panic!(), }; assert_eq!( message, "Required attribute field 'sid' on Open element missing." ); } #[test] fn test_invalid_stanza() { let elem: Element = "".parse().unwrap(); let error = Open::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::TextParseError(string)) => string, _ => panic!(), }; assert_eq!(message.to_string(), "Unknown value for 'stanza' attribute."); } } xmpp-parsers-0.22.0/src/ibr.rs000064400000000000000000000157511046102023000142700ustar 00000000000000// Copyright (c) 2017 Emmanuel Gil Peyrot // // 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 crate::data_forms::DataForm; use crate::iq::{IqGetPayload, IqResultPayload, IqSetPayload}; use crate::ns; use alloc::collections::BTreeMap; use minidom::Element; use xso::error::{Error, FromElementError}; /// Query for registering against a service. #[derive(Debug, Clone)] pub struct Query { /// Deprecated fixed list of possible fields to fill before the user can /// register. pub fields: BTreeMap, /// Whether this account is already registered. pub registered: bool, /// Whether to remove this account. pub remove: bool, /// A data form the user must fill before being allowed to register. pub form: Option, // Not yet implemented. //pub oob: Option, } impl IqGetPayload for Query {} impl IqSetPayload for Query {} impl IqResultPayload for Query {} impl TryFrom for Query { type Error = FromElementError; fn try_from(elem: Element) -> Result { check_self!(elem, "query", REGISTER, "IBR query"); let mut query = Query { registered: false, fields: BTreeMap::new(), remove: false, form: None, }; for child in elem.children() { let namespace = child.ns(); if namespace == ns::REGISTER { let name = child.name(); let fields = vec![ "address", "city", "date", "email", "first", "instructions", "key", "last", "misc", "name", "nick", "password", "phone", "state", "text", "url", "username", "zip", ]; if fields.binary_search(&name).is_ok() { query.fields.insert(name.to_owned(), child.text()); } else if name == "registered" { query.registered = true; } else if name == "remove" { query.remove = true; } else { return Err(Error::Other("Wrong field in ibr element.").into()); } } else if child.is("x", ns::DATA_FORMS) { query.form = Some(DataForm::try_from(child.clone())?); } else { return Err(Error::Other("Unknown child in ibr element.").into()); } } Ok(query) } } impl From for Element { fn from(query: Query) -> Element { Element::builder("query", ns::REGISTER) .append_all(if query.registered { Some(Element::builder("registered", ns::REGISTER)) } else { None }) .append_all( query .fields .into_iter() .map(|(name, value)| Element::builder(name, ns::REGISTER).append(value)), ) .append_all(if query.remove { Some(Element::builder("remove", ns::REGISTER)) } else { None }) .append_all(query.form.map(Element::from)) .build() } } #[cfg(test)] mod tests { use super::*; #[cfg(target_pointer_width = "32")] #[test] fn test_size() { assert_size!(Query, 56); } #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(Query, 112); } #[test] fn test_simple() { let elem: Element = "".parse().unwrap(); Query::try_from(elem).unwrap(); } #[test] fn test_ex2() { let elem: Element = r#" Choose a username and password for use with this service. Please also provide your email address. "# .parse() .unwrap(); let query = Query::try_from(elem).unwrap(); assert_eq!(query.registered, false); assert_eq!(query.fields["instructions"], "\n Choose a username and password for use with this service.\n Please also provide your email address.\n "); assert_eq!(query.fields["username"], ""); assert_eq!(query.fields["password"], ""); assert_eq!(query.fields["email"], ""); assert_eq!(query.fields.contains_key("name"), false); // FIXME: HashMap doesn’t keep the order right. //let elem2 = query.into(); //assert_eq!(elem, elem2); } #[test] fn test_ex9() { let elem: Element = "Use the enclosed form to register. If your Jabber client does not support Data Forms, visit http://www.shakespeare.lit/contests.phpContest RegistrationPlease provide the following information to sign up for our special contests!jabber:iq:register" .parse() .unwrap(); let elem1 = elem.clone(); let query = Query::try_from(elem).unwrap(); assert_eq!(query.registered, false); assert!(!query.fields["instructions"].is_empty()); let form = query.form.clone().unwrap(); assert!(!form.instructions.unwrap().is_empty()); let elem2 = query.into(); assert_eq!(elem1, elem2); } #[test] fn test_ex10() { let elem: Element = "jabber:iq:registerJulietCapuletjuliet@capulet.comF" .parse() .unwrap(); let elem1 = elem.clone(); let query = Query::try_from(elem).unwrap(); assert_eq!(query.registered, false); for _ in &query.fields { panic!(); } let elem2 = query.into(); assert_eq!(elem1, elem2); } } xmpp-parsers-0.22.0/src/idle.rs000064400000000000000000000136401046102023000144240ustar 00000000000000// Copyright (c) 2017 Emmanuel Gil Peyrot // // 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 xso::{AsXml, FromXml}; use crate::date::DateTime; use crate::ns; use crate::presence::PresencePayload; /// Represents the last time the user interacted with their system. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::IDLE, name = "idle")] pub struct Idle { /// The time at which the user stopped interacting. #[xml(attribute)] pub since: DateTime, } impl PresencePayload for Idle {} #[cfg(test)] mod tests { use super::*; use core::str::FromStr; use minidom::Element; use xso::error::{Error, FromElementError}; #[test] fn test_size() { assert_size!(Idle, 16); } #[test] fn test_simple() { let elem: Element = "" .parse() .unwrap(); Idle::try_from(elem).unwrap(); } #[test] #[cfg_attr(feature = "disable-validation", should_panic = "Result::unwrap_err")] fn test_invalid_child() { let elem: Element = "" .parse() .unwrap(); let error = Idle::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, other => panic!("unexpected result: {:?}", other), }; assert_eq!(message, "Unknown child in Idle element."); } #[test] fn test_invalid_id() { let elem: Element = "".parse().unwrap(); let error = Idle::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!( message, "Required attribute field 'since' on Idle element missing." ); } #[test] fn test_invalid_date() { // There is no thirteenth month. let elem: Element = "" .parse() .unwrap(); let error = Idle::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::TextParseError(string)) if string.is::() => { string } other => panic!("unexpected result: {:?}", other), }; assert_eq!(message.to_string(), "input is out of range"); // Timezone ≥24:00 aren’t allowed. let elem: Element = "" .parse() .unwrap(); let error = Idle::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::TextParseError(string)) if string.is::() => { string } _ => panic!(), }; assert_eq!(message.to_string(), "input is out of range"); // Timezone without the : separator aren’t allowed. let elem: Element = "" .parse() .unwrap(); let error = Idle::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::TextParseError(string)) if string.is::() => { string } _ => panic!(), }; assert_eq!(message.to_string(), "input contains invalid characters"); // No seconds, error message could be improved. let elem: Element = "" .parse() .unwrap(); let error = Idle::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::TextParseError(string)) if string.is::() => { string } _ => panic!(), }; assert_eq!(message.to_string(), "input contains invalid characters"); // TODO: maybe we’ll want to support this one, as per XEP-0082 §4. let elem: Element = "" .parse() .unwrap(); let error = Idle::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::TextParseError(string)) if string.is::() => { string } _ => panic!(), }; assert_eq!(message.to_string(), "input contains invalid characters"); // No timezone. let elem: Element = "" .parse() .unwrap(); let error = Idle::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::TextParseError(string)) if string.is::() => { string } _ => panic!(), }; assert_eq!(message.to_string(), "premature end of input"); } #[test] fn test_serialise() { let elem: Element = "" .parse() .unwrap(); let idle = Idle { since: DateTime::from_str("2017-05-21T20:19:55+01:00").unwrap(), }; let elem2 = idle.into(); assert_eq!(elem, elem2); } } xmpp-parsers-0.22.0/src/iq.rs000064400000000000000000000462201046102023000141200ustar 00000000000000// Copyright (c) 2017 Emmanuel Gil Peyrot // Copyright (c) 2017 Maxime “pep” Buquet // // 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 crate::ns; use crate::stanza_error::StanzaError; use jid::Jid; use minidom::Element; use xso::{AsXml, FromXml}; /// Should be implemented on every known payload of an ``. pub trait IqGetPayload: TryFrom + Into {} /// Should be implemented on every known payload of an ``. pub trait IqSetPayload: TryFrom + Into {} /// Should be implemented on every known payload of an ``. pub trait IqResultPayload: TryFrom + Into {} /// Metadata of an IQ stanza. pub struct IqHeader { /// The sender JID. pub from: Option, /// The reciepient JID. pub to: Option, /// The stanza's ID. pub id: String, } impl IqHeader { /// Combine a header with [`IqPayload`] to create a full [`Iq`] stanza. pub fn assemble(self, data: IqPayload) -> Iq { data.assemble(self) } } /// Payload of an IQ request stanza. pub enum IqRequestPayload { /// Payload of a type='get' stanza. Get(Element), /// Payload of a type='set' stanza. Set(Element), } /// Payload of an IQ stanza, by type. pub enum IqPayload { /// Payload of a type='get' stanza. Get(Element), /// Payload of a type='set' stanza. Set(Element), /// Payload of a type='result' stanza. Result(Option), /// The error carried in a type='error' stanza. Error(StanzaError), } impl IqPayload { /// Combine the data with an [`IqHeader`] to create a full [`Iq`] stanza. pub fn assemble(self, IqHeader { from, to, id }: IqHeader) -> Iq { match self { Self::Get(payload) => Iq::Get { from, to, id, payload, }, Self::Set(payload) => Iq::Set { from, to, id, payload, }, Self::Result(payload) => Iq::Result { from, to, id, payload, }, Self::Error(error) => Iq::Error { from, to, id, payload: None, error, }, } } } /// The main structure representing the `` stanza. #[derive(FromXml, AsXml, Debug, Clone, PartialEq)] #[xml(namespace = ns::DEFAULT_NS, name = "iq", attribute = "type", exhaustive)] pub enum Iq { /// An `` stanza. #[xml(value = "get")] Get { /// The JID emitting this stanza. #[xml(attribute(default))] from: Option, /// The recipient of this stanza. #[xml(attribute(default))] to: Option, /// The @id attribute of this stanza, which is required in order to match a /// request with its result/error. #[xml(attribute)] id: String, /// The payload content of this stanza. #[xml(element(n = 1))] payload: Element, }, /// An `` stanza. #[xml(value = "set")] Set { /// The JID emitting this stanza. #[xml(attribute(default))] from: Option, /// The recipient of this stanza. #[xml(attribute(default))] to: Option, /// The @id attribute of this stanza, which is required in order to match a /// request with its result/error. #[xml(attribute)] id: String, /// The payload content of this stanza. #[xml(element(n = 1))] payload: Element, }, /// An `` stanza. #[xml(value = "result")] Result { /// The JID emitting this stanza. #[xml(attribute(default))] from: Option, /// The recipient of this stanza. #[xml(attribute(default))] to: Option, /// The @id attribute of this stanza, which is required in order to match a /// request with its result/error. #[xml(attribute)] id: String, /// The payload content of this stanza. #[xml(element(n = 1, default))] payload: Option, }, /// An `` stanza. #[xml(value = "error")] Error { /// The JID emitting this stanza. #[xml(attribute(default))] from: Option, /// The recipient of this stanza. #[xml(attribute(default))] to: Option, /// The @id attribute of this stanza, which is required in order to match a /// request with its result/error. #[xml(attribute)] id: String, /// The error carried by this stanza. #[xml(child)] error: StanzaError, /// The optional payload content which caused the error. /// /// As per /// [RFC 6120 § 8.3.1](https://datatracker.ietf.org/doc/html/rfc6120#section-8.3.1), /// the emitter of an error stanza MAY include the original XML which /// caused the error. However, recipients MUST NOT rely on this. #[xml(element(n = 1, default))] payload: Option, }, } impl Iq { /// Assemble a new Iq stanza from an [`IqHeader`] and the given /// [`IqPayload`]. pub fn assemble(header: IqHeader, data: IqPayload) -> Self { data.assemble(header) } /// Creates an `` stanza containing a get request. pub fn from_get>(id: S, payload: impl IqGetPayload) -> Iq { Iq::Get { from: None, to: None, id: id.into(), payload: payload.into(), } } /// Creates an `` stanza containing a set request. pub fn from_set>(id: S, payload: impl IqSetPayload) -> Iq { Iq::Set { from: None, to: None, id: id.into(), payload: payload.into(), } } /// Creates an empty `` stanza. pub fn empty_result>(to: Jid, id: S) -> Iq { Iq::Result { from: None, to: Some(to), id: id.into(), payload: None, } } /// Creates an `` stanza containing a result. pub fn from_result>(id: S, payload: Option) -> Iq { Iq::Result { from: None, to: None, id: id.into(), payload: payload.map(Into::into), } } /// Creates an `` stanza containing an error. pub fn from_error>(id: S, payload: StanzaError) -> Iq { Iq::Error { from: None, to: None, id: id.into(), error: payload, payload: None, } } /// Sets the recipient of this stanza. pub fn with_to(mut self, to: Jid) -> Iq { *self.to_mut() = Some(to); self } /// Sets the emitter of this stanza. pub fn with_from(mut self, from: Jid) -> Iq { *self.from_mut() = Some(from); self } /// Sets the id of this stanza, in order to later match its response. pub fn with_id(mut self, id: String) -> Iq { *self.id_mut() = id; self } /// Access the sender address. pub fn from(&self) -> Option<&Jid> { match self { Self::Get { from, .. } | Self::Set { from, .. } | Self::Result { from, .. } | Self::Error { from, .. } => from.as_ref(), } } /// Access the sender address, mutably. pub fn from_mut(&mut self) -> &mut Option { match self { Self::Get { ref mut from, .. } | Self::Set { ref mut from, .. } | Self::Result { ref mut from, .. } | Self::Error { ref mut from, .. } => from, } } /// Access the recipient address. pub fn to(&self) -> Option<&Jid> { match self { Self::Get { to, .. } | Self::Set { to, .. } | Self::Result { to, .. } | Self::Error { to, .. } => to.as_ref(), } } /// Access the recipient address, mutably. pub fn to_mut(&mut self) -> &mut Option { match self { Self::Get { ref mut to, .. } | Self::Set { ref mut to, .. } | Self::Result { ref mut to, .. } | Self::Error { ref mut to, .. } => to, } } /// Access the id. pub fn id(&self) -> &str { match self { Self::Get { id, .. } | Self::Set { id, .. } | Self::Result { id, .. } | Self::Error { id, .. } => id.as_str(), } } /// Access the id mutably. pub fn id_mut(&mut self) -> &mut String { match self { Self::Get { ref mut id, .. } | Self::Set { ref mut id, .. } | Self::Result { ref mut id, .. } | Self::Error { ref mut id, .. } => id, } } /// Split the IQ stanza in its metadata and data. /// /// Note that this discards the optional original error-inducing /// [`payload`][`Self::Error::payload`] of the /// [`Iq::Error`][`Self::Error`] variant. pub fn split(self) -> (IqHeader, IqPayload) { match self { Self::Get { from, to, id, payload, } => (IqHeader { from, to, id }, IqPayload::Get(payload)), Self::Set { from, to, id, payload, } => (IqHeader { from, to, id }, IqPayload::Set(payload)), Self::Result { from, to, id, payload, } => (IqHeader { from, to, id }, IqPayload::Result(payload)), Self::Error { from, to, id, error, payload: _, } => (IqHeader { from, to, id }, IqPayload::Error(error)), } } /// Return the [`IqHeader`] of this stanza, discarding the payload. pub fn into_header(self) -> IqHeader { self.split().0 } /// Return the [`IqPayload`] of this stanza, discarding the header. /// /// Note that this also discards the optional original error-inducing /// [`payload`][`Self::Error::payload`] of the /// [`Iq::Error`][`Self::Error`] variant. pub fn into_payload(self) -> IqPayload { self.split().1 } } #[cfg(test)] mod tests { use super::*; use crate::disco::DiscoInfoQuery; use crate::stanza_error::{DefinedCondition, ErrorType}; use xso::error::{Error, FromElementError}; #[cfg(target_pointer_width = "32")] #[test] fn test_size() { assert_size!(IqHeader, 44); assert_size!(IqPayload, 108); assert_size!(Iq, 212); } #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(IqHeader, 88); assert_size!(IqPayload, 216); assert_size!(Iq, 424); } #[test] fn test_require_type() { #[cfg(not(feature = "component"))] let elem: Element = "".parse().unwrap(); #[cfg(feature = "component")] let elem: Element = "".parse().unwrap(); let error = Iq::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!(message, "Missing discriminator attribute."); } #[test] fn test_require_id() { for type_ in ["get", "set", "result", "error"] { #[cfg(not(feature = "component"))] let elem: Element = format!("", type_) .parse() .unwrap(); #[cfg(feature = "component")] let elem: Element = format!("", type_) .parse() .unwrap(); let error = Iq::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; // Slicing here, because the rest of the error message is specific // about the enum variant. assert_eq!(&message[..33], "Required attribute field 'id' on "); } } #[test] fn test_get() { #[cfg(not(feature = "component"))] let elem: Element = " " .parse() .unwrap(); #[cfg(feature = "component")] let elem: Element = " " .parse() .unwrap(); let iq = Iq::try_from(elem).unwrap(); let query: Element = "".parse().unwrap(); assert_eq!(iq.from(), None); assert_eq!(iq.to(), None); assert_eq!(iq.id(), "foo"); assert!(match iq { Iq::Get { payload, .. } => payload == query, _ => false, }); } #[test] fn test_set() { #[cfg(not(feature = "component"))] let elem: Element = " " .parse() .unwrap(); #[cfg(feature = "component")] let elem: Element = " " .parse() .unwrap(); let iq = Iq::try_from(elem).unwrap(); let vcard: Element = "".parse().unwrap(); assert_eq!(iq.from(), None); assert_eq!(iq.to(), None); assert_eq!(iq.id(), "vcard"); assert!(match iq { Iq::Set { payload, .. } => payload == vcard, _ => false, }); } #[test] fn test_result_empty() { #[cfg(not(feature = "component"))] let elem: Element = "" .parse() .unwrap(); #[cfg(feature = "component")] let elem: Element = "" .parse() .unwrap(); let iq = Iq::try_from(elem).unwrap(); assert_eq!(iq.from(), None); assert_eq!(iq.to(), None); assert_eq!(iq.id(), "res"); assert!(match iq { Iq::Result { payload: None, .. } => true, _ => false, }); } #[test] fn test_result() { #[cfg(not(feature = "component"))] let elem: Element = " " .parse() .unwrap(); #[cfg(feature = "component")] let elem: Element = " " .parse() .unwrap(); let iq = Iq::try_from(elem).unwrap(); let query: Element = "" .parse() .unwrap(); assert_eq!(iq.from(), None); assert_eq!(iq.to(), None); assert_eq!(iq.id(), "res"); assert!(match iq { Iq::Result { payload: Some(element), .. } => element == query, _ => false, }); } #[test] fn test_error() { #[cfg(not(feature = "component"))] let elem: Element = " " .parse() .unwrap(); #[cfg(feature = "component")] let elem: Element = " " .parse() .unwrap(); let iq = Iq::try_from(elem).unwrap(); assert_eq!(iq.from(), None); assert_eq!(iq.to(), None); assert_eq!(iq.id(), "err1"); match iq { Iq::Error { error, .. } => { assert_eq!(error.type_, ErrorType::Cancel); assert_eq!(error.by, None); assert_eq!( error.defined_condition, DefinedCondition::ServiceUnavailable ); assert_eq!(error.texts.len(), 0); assert_eq!(error.other, None); } _ => panic!(), } } #[test] fn test_children_invalid() { #[cfg(not(feature = "component"))] let elem: Element = "" .parse() .unwrap(); #[cfg(feature = "component")] let elem: Element = "" .parse() .unwrap(); let error = Iq::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!(message, "Missing child field 'error' in Iq::Error element."); } #[test] fn test_serialise() { #[cfg(not(feature = "component"))] let elem: Element = "" .parse() .unwrap(); #[cfg(feature = "component")] let elem: Element = "" .parse() .unwrap(); let iq2 = Iq::Result { from: None, to: None, id: String::from("res"), payload: None, }; let elem2 = iq2.into(); assert_eq!(elem, elem2); } #[test] fn test_disco() { #[cfg(not(feature = "component"))] let elem: Element = "".parse().unwrap(); #[cfg(feature = "component")] let elem: Element = "".parse().unwrap(); let iq = Iq::try_from(elem).unwrap(); let disco_info = match iq { Iq::Get { payload, .. } => DiscoInfoQuery::try_from(payload).unwrap(), _ => panic!(), }; assert!(disco_info.node.is_none()); } } xmpp-parsers-0.22.0/src/jid_prep.rs000064400000000000000000000042101046102023000152740ustar 00000000000000// Copyright (c) 2019 Emmanuel Gil Peyrot // // 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 xso::{AsXml, FromXml}; use jid::Jid; use crate::iq::{IqGetPayload, IqResultPayload}; use crate::ns; /// Request from a client to stringprep/PRECIS a string into a JID. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::JID_PREP, name = "jid")] pub struct JidPrepQuery { /// The potential JID. #[xml(text)] pub data: String, } impl IqGetPayload for JidPrepQuery {} impl JidPrepQuery { /// Create a new JID Prep query. pub fn new>(jid: J) -> JidPrepQuery { JidPrepQuery { data: jid.into() } } } /// Response from the server with the stringprep’d/PRECIS’d JID. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::JID_PREP, name = "jid")] pub struct JidPrepResponse { /// The JID. #[xml(text)] pub jid: Jid, } impl IqResultPayload for JidPrepResponse {} #[cfg(test)] mod tests { use super::*; use jid::FullJid; use minidom::Element; #[cfg(target_pointer_width = "32")] #[test] fn test_size() { assert_size!(JidPrepQuery, 12); assert_size!(JidPrepResponse, 16); } #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(JidPrepQuery, 24); assert_size!(JidPrepResponse, 32); } #[test] fn simple() { let elem: Element = "ROMeo@montague.lit/orchard" .parse() .unwrap(); let query = JidPrepQuery::try_from(elem).unwrap(); assert_eq!(query.data, "ROMeo@montague.lit/orchard"); let elem: Element = "romeo@montague.lit/orchard" .parse() .unwrap(); let response = JidPrepResponse::try_from(elem).unwrap(); assert_eq!( response.jid, FullJid::new("romeo@montague.lit/orchard").unwrap() ); } } xmpp-parsers-0.22.0/src/jingle.rs000064400000000000000000000711331046102023000147600ustar 00000000000000// Copyright (c) 2017 Emmanuel Gil Peyrot // // 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 xso::{AsXml, FromXml}; use crate::iq::IqSetPayload; use crate::jingle_grouping::Group; use crate::jingle_ibb::Transport as IbbTransport; use crate::jingle_ice_udp::Transport as IceUdpTransport; use crate::jingle_rtp::Description as RtpDescription; use crate::jingle_s5b::Transport as Socks5Transport; use crate::ns; use alloc::{collections::BTreeMap, fmt}; use jid::Jid; use minidom::Element; use xso::error::Error; generate_attribute!( /// The action attribute. Action, "action", { /// Accept a content-add action received from another party. ContentAccept => "content-accept", /// Add one or more new content definitions to the session. ContentAdd => "content-add", /// Change the directionality of media sending. ContentModify => "content-modify", /// Reject a content-add action received from another party. ContentReject => "content-reject", /// Remove one or more content definitions from the session. ContentRemove => "content-remove", /// Exchange information about parameters for an application type. DescriptionInfo => "description-info", /// Exchange information about security preconditions. SecurityInfo => "security-info", /// Definitively accept a session negotiation. SessionAccept => "session-accept", /// Send session-level information, such as a ping or a ringing message. SessionInfo => "session-info", /// Request negotiation of a new Jingle session. SessionInitiate => "session-initiate", /// End an existing session. SessionTerminate => "session-terminate", /// Accept a transport-replace action received from another party. TransportAccept => "transport-accept", /// Exchange transport candidates. TransportInfo => "transport-info", /// Reject a transport-replace action received from another party. TransportReject => "transport-reject", /// Redefine a transport method or replace it with a different method. TransportReplace => "transport-replace", } ); generate_attribute!( /// Which party originally generated the content type. Creator, "creator", { /// This content was created by the initiator of this session. Initiator => "initiator", /// This content was created by the responder of this session. Responder => "responder", } ); generate_attribute!( /// Which parties in the session will be generating content. Senders, "senders", { /// Both parties can send for this content. Both => "both", /// Only the initiator can send for this content. Initiator => "initiator", /// No one can send for this content. None => "none", /// Only the responder can send for this content. Responder => "responder", }, Default = Both ); generate_attribute!( /// How the content definition is to be interpreted by the recipient. The /// meaning of this attribute matches the "Content-Disposition" header as /// defined in RFC 2183 and applied to SIP by RFC 3261. /// /// Possible values are defined here: /// Disposition, "disposition", { /// Displayed automatically. Inline => "inline", /// User controlled display. Attachment => "attachment", /// Process as form response. FormData => "form-data", /// Tunneled content to be processed silently. Signal => "signal", /// The body is a custom ring tone to alert the user. Alert => "alert", /// The body is displayed as an icon to the user. Icon => "icon", /// The body should be displayed to the user. Render => "render", /// The body contains a list of URIs that indicates the recipients of /// the request. RecipientListHistory => "recipient-list-history", /// The body describes a communications session, for example, an /// [RFC2327](https://www.rfc-editor.org/rfc/rfc2327) SDP body. Session => "session", /// Authenticated Identity Body. Aib => "aib", /// The body describes an early communications session, for example, /// and [RFC2327](https://www.rfc-editor.org/rfc/rfc2327) SDP body. EarlySession => "early-session", /// The body includes a list of URIs to which URI-list services are to /// be applied. RecipientList => "recipient-list", /// The payload of the message carrying this Content-Disposition header /// field value is an Instant Message Disposition Notification as /// requested in the corresponding Instant Message. Notification => "notification", /// The body needs to be handled according to a reference to the body /// that is located in the same SIP message as the body. ByReference => "by-reference", /// The body contains information associated with an Info Package. InfoPackage => "info-package", /// The body describes either metadata about the RS or the reason for /// the metadata snapshot request as determined by the MIME value /// indicated in the Content-Type. RecordingSession => "recording-session", }, Default = Session ); generate_id!( /// An unique identifier in a session, referencing a /// [struct.Content.html](Content element). ContentId ); /// Enum wrapping all of the various supported descriptions of a Content. #[derive(AsXml, Debug, Clone, PartialEq)] #[xml()] pub enum Description { /// Jingle RTP Sessions (XEP-0167) description. #[xml(transparent)] Rtp(RtpDescription), /// To be used for any description that isn’t known at compile-time. // TODO: replace with `#[xml(element, name = ..)]` once we have it. #[xml(transparent)] Unknown(Element), } impl TryFrom for Description { type Error = Error; fn try_from(elem: Element) -> Result { Ok(if elem.is("description", ns::JINGLE_RTP) { Description::Rtp(RtpDescription::try_from(elem)?) } else if elem.name() == "description" { Description::Unknown(elem) } else { return Err(Error::Other("Invalid description.")); }) } } impl ::xso::FromXml for Description { type Builder = ::xso::minidom_compat::FromEventsViaElement; fn from_events( qname: ::xso::exports::rxml::QName, attrs: ::xso::exports::rxml::AttrMap, _ctx: &::xso::Context<'_>, ) -> Result { if qname.1 != "description" { return Err(::xso::error::FromEventsError::Mismatch { name: qname, attrs }); } Self::Builder::new(qname, attrs) } } impl From for Description { fn from(desc: RtpDescription) -> Description { Description::Rtp(desc) } } /// Enum wrapping all of the various supported transports of a Content. #[derive(AsXml, Debug, Clone, PartialEq)] #[xml()] pub enum Transport { /// Jingle ICE-UDP Bytestreams (XEP-0176) transport. #[xml(transparent)] IceUdp(IceUdpTransport), /// Jingle In-Band Bytestreams (XEP-0261) transport. #[xml(transparent)] Ibb(IbbTransport), /// Jingle SOCKS5 Bytestreams (XEP-0260) transport. #[xml(transparent)] Socks5(Socks5Transport), /// To be used for any transport that isn’t known at compile-time. // TODO: replace with `#[xml(element, name = ..)]` once we have it. #[xml(transparent)] Unknown(Element), } impl TryFrom for Transport { type Error = Error; fn try_from(elem: Element) -> Result { Ok(if elem.is("transport", ns::JINGLE_ICE_UDP) { Transport::IceUdp(IceUdpTransport::try_from(elem)?) } else if elem.is("transport", ns::JINGLE_IBB) { Transport::Ibb(IbbTransport::try_from(elem)?) } else if elem.is("transport", ns::JINGLE_S5B) { Transport::Socks5(Socks5Transport::try_from(elem)?) } else if elem.name() == "transport" { Transport::Unknown(elem) } else { return Err(Error::Other("Invalid transport.")); }) } } impl ::xso::FromXml for Transport { type Builder = ::xso::minidom_compat::FromEventsViaElement; fn from_events( qname: ::xso::exports::rxml::QName, attrs: ::xso::exports::rxml::AttrMap, _ctx: &::xso::Context<'_>, ) -> Result { if qname.1 != "transport" { return Err(::xso::error::FromEventsError::Mismatch { name: qname, attrs }); } Self::Builder::new(qname, attrs) } } impl From for Transport { fn from(transport: IceUdpTransport) -> Transport { Transport::IceUdp(transport) } } impl From for Transport { fn from(transport: IbbTransport) -> Transport { Transport::Ibb(transport) } } impl From for Transport { fn from(transport: Socks5Transport) -> Transport { Transport::Socks5(transport) } } /// A security element inside a Jingle content, stubbed for now. #[derive(FromXml, AsXml, Debug, Clone, PartialEq)] #[xml(namespace = ns::JINGLE, name = "security")] pub struct Security; /// Describes a session’s content, there can be multiple content in one /// session. #[derive(FromXml, AsXml, Debug, Clone, PartialEq)] #[xml(namespace = ns::JINGLE, name = "content")] pub struct Content { /// Who created this content. #[xml(attribute)] pub creator: Creator, /// How the content definition is to be interpreted by the recipient. #[xml(attribute(default))] pub disposition: Disposition, /// A per-session unique identifier for this content. #[xml(attribute)] pub name: ContentId, /// Who can send data for this content. #[xml(attribute(default))] pub senders: Senders, /// What to send. #[xml(child(default))] pub description: Option, /// How to send it. #[xml(child(default))] pub transport: Option, /// With which security. #[xml(child(default))] pub security: Option, } impl Content { /// Create a new content. pub fn new(creator: Creator, name: ContentId) -> Content { Content { creator, name, disposition: Disposition::Session, senders: Senders::Both, description: None, transport: None, security: None, } } /// Set how the content is to be interpreted by the recipient. pub fn with_disposition(mut self, disposition: Disposition) -> Content { self.disposition = disposition; self } /// Specify who can send data for this content. pub fn with_senders(mut self, senders: Senders) -> Content { self.senders = senders; self } /// Set the description of this content. pub fn with_description>(mut self, description: D) -> Content { self.description = Some(description.into()); self } /// Set the transport of this content. pub fn with_transport>(mut self, transport: T) -> Content { self.transport = Some(transport.into()); self } /// Set the security of this content. pub fn with_security(mut self, security: Security) -> Content { self.security = Some(security); self } } /// Lists the possible reasons to be included in a Jingle iq. #[derive(FromXml, AsXml, Debug, Clone, PartialEq)] #[xml(namespace = ns::JINGLE)] pub enum Reason { /// The party prefers to use an existing session with the peer rather than /// initiate a new session; the Jingle session ID of the alternative /// session SHOULD be provided as the XML character data of the \ /// child. #[xml(name = "alternative-session")] AlternativeSession { /// Session ID of the alternative session. #[xml(extract(namespace = ns::JINGLE, name = "sid", default, fields(text(type_ = String))))] sid: Option, }, /// The party is busy and cannot accept a session. #[xml(name = "busy")] Busy, /// The initiator wishes to formally cancel the session initiation request. #[xml(name = "cancel")] Cancel, /// The action is related to connectivity problems. #[xml(name = "connectivity-error")] ConnectivityError, /// The party wishes to formally decline the session. #[xml(name = "decline")] Decline, /// The session length has exceeded a pre-defined time limit (e.g., a /// meeting hosted at a conference service). #[xml(name = "expired")] Expired, /// The party has been unable to initialize processing related to the /// application type. #[xml(name = "failed-application")] FailedApplication, /// The party has been unable to establish connectivity for the transport /// method. #[xml(name = "failed-transport")] FailedTransport, /// The action is related to a non-specific application error. #[xml(name = "general-error")] GeneralError, /// The entity is going offline or is no longer available. #[xml(name = "gone")] Gone, /// The party supports the offered application type but does not support /// the offered or negotiated parameters. #[xml(name = "incompatible-parameters")] IncompatibleParameters, /// The action is related to media processing problems. #[xml(name = "media-error")] MediaError, /// The action is related to a violation of local security policies. #[xml(name = "security-error")] SecurityError, /// The action is generated during the normal course of state management /// and does not reflect any error. #[xml(name = "success")] Success, /// A request has not been answered so the sender is timing out the /// request. #[xml(name = "timeout")] Timeout, /// The party supports none of the offered application types. #[xml(name = "unsupported-applications")] UnsupportedApplications, /// The party supports none of the offered transport methods. #[xml(name = "unsupported-transports")] UnsupportedTransports, } type Lang = String; /// Informs the recipient of something. #[derive(FromXml, AsXml, Debug, Clone, PartialEq)] #[xml(namespace = ns::JINGLE, name = "reason")] pub struct ReasonElement { /// The list of possible reasons to be included in a Jingle iq. #[xml(child)] pub reason: Reason, /// A human-readable description of this reason. #[xml(extract(n = .., namespace = ns::JINGLE, name = "text", fields( lang(type_ = Lang, default), text(type_ = String), )))] pub texts: BTreeMap, } impl fmt::Display for ReasonElement { fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { write!(fmt, "{}", Element::from(self.reason.clone()).name())?; if let Some(text) = self.texts.get("en") { write!(fmt, ": {}", text)?; } else if let Some(text) = self.texts.get("") { write!(fmt, ": {}", text)?; } Ok(()) } } generate_id!( /// Unique identifier for a session between two JIDs. SessionId ); /// The main Jingle container, to be included in an iq stanza. #[derive(FromXml, AsXml, Debug, Clone, PartialEq)] #[xml(namespace = ns::JINGLE, name = "jingle")] pub struct Jingle { /// The action to execute on both ends. #[xml(attribute)] pub action: Action, /// Who the initiator is. #[xml(attribute(default))] pub initiator: Option, /// Who the responder is. #[xml(attribute(default))] pub responder: Option, /// Unique session identifier between two entities. #[xml(attribute)] pub sid: SessionId, /// A list of contents to be negotiated in this session. #[xml(child(n = ..))] pub contents: Vec, /// An optional reason. #[xml(child(default))] pub reason: Option, /// An optional grouping. #[xml(child(default))] pub group: Option, /// Payloads to be included. #[xml(child(n = ..))] pub other: Vec, } impl IqSetPayload for Jingle {} impl Jingle { /// Create a new Jingle element. pub fn new(action: Action, sid: SessionId) -> Jingle { Jingle { action, sid, initiator: None, responder: None, contents: Vec::new(), reason: None, group: None, other: Vec::new(), } } /// Set the initiator’s JID. pub fn with_initiator(mut self, initiator: Jid) -> Jingle { self.initiator = Some(initiator); self } /// Set the responder’s JID. pub fn with_responder(mut self, responder: Jid) -> Jingle { self.responder = Some(responder); self } /// Add a content to this Jingle container. pub fn add_content(mut self, content: Content) -> Jingle { self.contents.push(content); self } /// Set the reason in this Jingle container. pub fn set_reason(mut self, reason: ReasonElement) -> Jingle { self.reason = Some(reason); self } /// Set the grouping in this Jingle container. pub fn set_group(mut self, group: Group) -> Jingle { self.group = Some(group); self } } #[cfg(test)] mod tests { use super::*; use xso::error::FromElementError; #[cfg(target_pointer_width = "32")] #[test] fn test_size() { assert_size!(Action, 1); assert_size!(Creator, 1); assert_size!(Senders, 1); assert_size!(Disposition, 1); assert_size!(ContentId, 12); assert_size!(Content, 156); assert_size!(Reason, 12); assert_size!(ReasonElement, 24); assert_size!(SessionId, 12); assert_size!(Jingle, 112); } #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(Action, 1); assert_size!(Creator, 1); assert_size!(Senders, 1); assert_size!(Disposition, 1); assert_size!(ContentId, 24); assert_size!(Content, 312); assert_size!(Reason, 24); assert_size!(ReasonElement, 48); assert_size!(SessionId, 24); assert_size!(Jingle, 224); } #[test] fn test_simple() { let elem: Element = "" .parse() .unwrap(); let jingle = Jingle::try_from(elem).unwrap(); assert_eq!(jingle.action, Action::SessionInitiate); assert_eq!(jingle.sid, SessionId(String::from("coucou"))); } #[test] fn test_invalid_jingle() { let elem: Element = "".parse().unwrap(); let error = Jingle::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!( message, "Required attribute field 'action' on Jingle element missing." ); let elem: Element = "" .parse() .unwrap(); let error = Jingle::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!( message, "Required attribute field 'sid' on Jingle element missing." ); let elem: Element = "" .parse() .unwrap(); let error = Jingle::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::TextParseError(string)) => string, _ => panic!(), }; assert_eq!(message.to_string(), "Unknown value for 'action' attribute."); } #[test] fn test_content() { let elem: Element = "".parse().unwrap(); let jingle = Jingle::try_from(elem).unwrap(); assert_eq!(jingle.contents[0].creator, Creator::Initiator); assert_eq!(jingle.contents[0].name, ContentId(String::from("coucou"))); assert_eq!(jingle.contents[0].senders, Senders::Both); assert_eq!(jingle.contents[0].disposition, Disposition::Session); let elem: Element = "".parse().unwrap(); let jingle = Jingle::try_from(elem).unwrap(); assert_eq!(jingle.contents[0].senders, Senders::Both); let elem: Element = "".parse().unwrap(); let jingle = Jingle::try_from(elem).unwrap(); assert_eq!(jingle.contents[0].disposition, Disposition::EarlySession); } #[test] fn test_invalid_content() { let elem: Element = "".parse().unwrap(); let error = Jingle::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!( message, "Required attribute field 'creator' on Content element missing." ); let elem: Element = "".parse().unwrap(); let error = Jingle::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!( message, "Required attribute field 'name' on Content element missing." ); let elem: Element = "".parse().unwrap(); let error = Jingle::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::TextParseError(string)) => string, other => panic!("unexpected result: {:?}", other), }; assert_eq!( message.to_string(), "Unknown value for 'creator' attribute." ); let elem: Element = "".parse().unwrap(); let error = Jingle::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::TextParseError(string)) => string, _ => panic!(), }; assert_eq!( message.to_string(), "Unknown value for 'senders' attribute." ); let elem: Element = "".parse().unwrap(); let error = Jingle::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::TextParseError(string)) => string, _ => panic!(), }; assert_eq!( message.to_string(), "Unknown value for 'senders' attribute." ); } #[test] fn test_reason() { let elem: Element = "".parse().unwrap(); let jingle = Jingle::try_from(elem).unwrap(); let reason = jingle.reason.unwrap(); assert_eq!(reason.reason, Reason::Success); assert_eq!(reason.texts, BTreeMap::new()); let elem: Element = "coucou".parse().unwrap(); let jingle = Jingle::try_from(elem).unwrap(); let reason = jingle.reason.unwrap(); assert_eq!(reason.reason, Reason::Success); assert_eq!(reason.texts.get(""), Some(&String::from("coucou"))); } #[test] fn test_missing_reason_text() { let elem: Element = "".parse().unwrap(); let error = Jingle::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!( message, "Missing child field 'reason' in ReasonElement element." ); } #[test] #[cfg_attr(feature = "disable-validation", should_panic = "Result::unwrap_err")] fn test_invalid_child_in_reason() { let elem: Element = "".parse().unwrap(); let error = Jingle::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!(message, "Unknown child in ReasonElement element."); } #[test] fn test_multiple_reason_children() { let elem: Element = "".parse().unwrap(); let error = Jingle::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!( message, "Jingle element must not have more than one child in field 'reason'." ); // TODO: Reenable this test once xso is able to validate that no more than one text is // there for every xml:lang. /* let elem: Element = "".parse().unwrap(); let error = Jingle::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!(message, "Text element present twice for the same xml:lang."); */ } #[test] fn test_serialize_jingle() { let reference: Element = "" .parse() .unwrap(); let jingle = Jingle { action: Action::SessionInitiate, initiator: None, responder: None, sid: SessionId(String::from("a73sjjvkla37jfea")), contents: vec![Content { creator: Creator::Initiator, disposition: Disposition::default(), name: ContentId(String::from("this-is-a-stub")), senders: Senders::default(), description: Some(Description::Unknown( Element::builder("description", "urn:xmpp:jingle:apps:stub:0").build(), )), transport: Some(Transport::Unknown( Element::builder("transport", "urn:xmpp:jingle:transports:stub:0").build(), )), security: None, }], reason: None, group: None, other: vec![], }; let serialized: Element = jingle.into(); assert_eq!(serialized, reference); } } xmpp-parsers-0.22.0/src/jingle_dtls_srtp.rs000064400000000000000000000066131046102023000170570ustar 00000000000000// Copyright (c) 2019 Emmanuel Gil Peyrot // // 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 xso::{text::ColonSeparatedHex, AsXml, FromXml}; use crate::hashes::{Algo, Hash}; use crate::ns; use xso::error::Error; generate_attribute!( /// Indicates which of the end points should initiate the TCP connection establishment. Setup, "setup", { /// The endpoint will initiate an outgoing connection. Active => "active", /// The endpoint will accept an incoming connection. Passive => "passive", /// The endpoint is willing to accept an incoming connection or to initiate an outgoing /// connection. Actpass => "actpass", /* /// The endpoint does not want the connection to be established for the time being. /// /// Note that this value isn’t used, as per the XEP. Holdconn => "holdconn", */ } ); // TODO: use a hashes::Hash instead of two different fields here. /// Fingerprint of the key used for a DTLS handshake. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::JINGLE_DTLS, name = "fingerprint")] pub struct Fingerprint { /// The hash algorithm used for this fingerprint. #[xml(attribute)] pub hash: Algo, /// Indicates which of the end points should initiate the TCP connection establishment. #[xml(attribute)] pub setup: Setup, /// Hash value of this fingerprint. #[xml(text(codec = ColonSeparatedHex))] pub value: Vec, } impl Fingerprint { /// Create a new Fingerprint from a Setup and a Hash. pub fn from_hash(setup: Setup, hash: Hash) -> Fingerprint { Fingerprint { hash: hash.algo, setup, value: hash.hash, } } /// Create a new Fingerprint from a Setup and parsing the hash. pub fn from_colon_separated_hex( setup: Setup, algo: &str, hash: &str, ) -> Result { let algo = algo.parse()?; let hash = Hash::from_colon_separated_hex(algo, hash).map_err(Error::text_parse_error)?; Ok(Fingerprint::from_hash(setup, hash)) } } #[cfg(test)] mod tests { use super::*; use minidom::Element; #[cfg(target_pointer_width = "32")] #[test] fn test_size() { assert_size!(Setup, 1); assert_size!(Fingerprint, 28); } #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(Setup, 1); assert_size!(Fingerprint, 56); } #[test] fn test_ex1() { let elem: Element = "02:1A:CC:54:27:AB:EB:9C:53:3F:3E:4B:65:2E:7D:46:3F:54:42:CD:54:F1:7A:03:A2:7D:F9:B0:7F:46:19:B2" .parse() .unwrap(); let fingerprint = Fingerprint::try_from(elem).unwrap(); assert_eq!(fingerprint.setup, Setup::Actpass); assert_eq!(fingerprint.hash, Algo::Sha_256); assert_eq!( fingerprint.value, [ 2, 26, 204, 84, 39, 171, 235, 156, 83, 63, 62, 75, 101, 46, 125, 70, 63, 84, 66, 205, 84, 241, 122, 3, 162, 125, 249, 176, 127, 70, 25, 178 ] ); } } xmpp-parsers-0.22.0/src/jingle_ft.rs000064400000000000000000000421411046102023000154460ustar 00000000000000// Copyright (c) 2017 Emmanuel Gil Peyrot // // 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 xso::{error::Error, AsXml, FromXml}; use crate::date::DateTime; use crate::hashes::Hash; use crate::jingle::{ContentId, Creator}; use crate::ns; use alloc::collections::btree_map::BTreeMap; use core::str::FromStr; /// Represents a range in a file. #[derive(FromXml, AsXml, PartialEq, Debug, Clone, Default)] #[xml(namespace = ns::JINGLE_FT, name = "range")] pub struct Range { /// The offset in bytes from the beginning of the file. #[xml(attribute(default))] pub offset: u64, /// The length in bytes of the range, or None to be the entire /// remaining of the file. #[xml(attribute(default))] pub length: Option, /// List of hashes for this range. #[xml(child(n = ..))] pub hashes: Vec, } impl Range { /// Creates a new range. pub fn new() -> Range { Default::default() } } type Lang = String; /// Represents a file to be transferred. #[derive(FromXml, AsXml, Debug, Clone, Default)] #[xml(namespace = ns::JINGLE_FT, name = "file")] pub struct File { /// The date of last modification of this file. #[xml(extract(default, fields(text(type_ = DateTime))))] pub date: Option, /// The MIME type of this file. #[xml(extract(default, name = "media-type", fields(text(type_ = String))))] pub media_type: Option, /// The name of this file. #[xml(extract(default, fields(text(type_ = String))))] pub name: Option, /// The description of this file, possibly localised. #[xml(extract(n = .., name = "desc", fields( lang(type_ = Lang, default), text(type_ = String) )))] pub descs: BTreeMap, /// The size of this file, in bytes. #[xml(extract(default, fields(text(type_ = u64))))] pub size: Option, /// Used to request only a part of this file. #[xml(child(default))] pub range: Option, /// A list of hashes matching this entire file. #[xml(child(n = ..))] pub hashes: Vec, } impl File { /// Creates a new file descriptor. pub fn new() -> File { File::default() } /// Sets the date of last modification on this file. pub fn with_date(mut self, date: DateTime) -> File { self.date = Some(date); self } /// Sets the date of last modification on this file from an ISO-8601 /// string. pub fn with_date_str(mut self, date: &str) -> Result { self.date = Some(DateTime::from_str(date).map_err(Error::text_parse_error)?); Ok(self) } /// Sets the MIME type of this file. pub fn with_media_type(mut self, media_type: String) -> File { self.media_type = Some(media_type); self } /// Sets the name of this file. pub fn with_name(mut self, name: String) -> File { self.name = Some(name); self } /// Sets a description for this file. pub fn add_desc(mut self, lang: &str, desc: String) -> File { self.descs.insert(Lang::from(lang), desc); self } /// Sets the file size of this file, in bytes. pub fn with_size(mut self, size: u64) -> File { self.size = Some(size); self } /// Request only a range of this file. pub fn with_range(mut self, range: Range) -> File { self.range = Some(range); self } /// Add a hash on this file. pub fn add_hash(mut self, hash: Hash) -> File { self.hashes.push(hash); self } } /// A wrapper element for a file. #[derive(FromXml, AsXml, Debug, Clone, Default)] #[xml(namespace = ns::JINGLE_FT, name = "description")] pub struct Description { /// The actual file descriptor. #[xml(child)] pub file: File, } /// A checksum for checking that the file has been transferred correctly. #[derive(FromXml, AsXml, Debug, Clone)] #[xml(namespace = ns::JINGLE_FT, name = "checksum")] pub struct Checksum { /// The identifier of the file transfer content. #[xml(attribute)] pub name: ContentId, /// The creator of this file transfer. #[xml(attribute)] pub creator: Creator, /// The file being checksummed. #[xml(child)] pub file: File, } /// A notice that the file transfer has been completed. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::JINGLE_FT, name = "received")] pub struct Received { /// The content identifier of this Jingle session. #[xml(attribute)] pub name: ContentId, /// The creator of this file transfer. #[xml(attribute)] pub creator: Creator, } #[cfg(test)] mod tests { use super::*; use crate::hashes::Algo; use base64::{engine::general_purpose::STANDARD as Base64, Engine}; use minidom::Element; use xso::error::FromElementError; // Apparently, i686 and AArch32/PowerPC seem to disagree here. So instead // of trying to figure this out now, we just ignore the test. #[cfg(target_pointer_width = "32")] #[test] #[ignore] fn test_size() { assert_size!(Range, 32); assert_size!(File, 104); assert_size!(Description, 104); assert_size!(Checksum, 128); assert_size!(Received, 16); } #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(Range, 48); assert_size!(File, 176); assert_size!(Description, 176); assert_size!(Checksum, 208); assert_size!(Received, 32); } #[test] fn test_description() { let elem: Element = r#" text/plain test.txt 2015-07-26T21:46:00+01:00 6144 w0mcJylzCn+AfvuGdqkty2+KP48= "# .parse() .unwrap(); let desc = Description::try_from(elem).unwrap(); assert_eq!(desc.file.media_type, Some(String::from("text/plain"))); assert_eq!(desc.file.name, Some(String::from("test.txt"))); assert_eq!(desc.file.descs, BTreeMap::new()); assert_eq!( desc.file.date, DateTime::from_str("2015-07-26T21:46:00+01:00").ok() ); assert_eq!(desc.file.size, Some(6144u64)); assert_eq!(desc.file.range, None); assert_eq!(desc.file.hashes[0].algo, Algo::Sha_1); assert_eq!( desc.file.hashes[0].hash, Base64.decode("w0mcJylzCn+AfvuGdqkty2+KP48=").unwrap() ); } #[test] fn test_request() { let elem: Element = r#" w0mcJylzCn+AfvuGdqkty2+KP48= "# .parse() .unwrap(); let desc = Description::try_from(elem).unwrap(); assert_eq!(desc.file.media_type, None); assert_eq!(desc.file.name, None); assert_eq!(desc.file.descs, BTreeMap::new()); assert_eq!(desc.file.date, None); assert_eq!(desc.file.size, None); assert_eq!(desc.file.range, None); assert_eq!(desc.file.hashes[0].algo, Algo::Sha_1); assert_eq!( desc.file.hashes[0].hash, Base64.decode("w0mcJylzCn+AfvuGdqkty2+KP48=").unwrap() ); } #[test] // TODO: Reenable that test once we correctly treat same @xml:lang as errors! #[ignore] fn test_descs() { let elem: Element = r#" text/plain Fichier secret ! Secret file! w0mcJylzCn+AfvuGdqkty2+KP48= "# .parse() .unwrap(); let desc = Description::try_from(elem).unwrap(); assert_eq!( desc.file.descs.keys().cloned().collect::>(), ["en", "fr"] ); assert_eq!(desc.file.descs["en"], String::from("Secret file!")); assert_eq!(desc.file.descs["fr"], String::from("Fichier secret !")); let elem: Element = r#" text/plain Fichier secret ! Secret file! w0mcJylzCn+AfvuGdqkty2+KP48= "# .parse() .unwrap(); let error = Description::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!(message, "Desc element present twice for the same xml:lang."); } #[test] fn test_received_valid() { let elem: Element = "".parse().unwrap(); let received = Received::try_from(elem).unwrap(); assert_eq!(received.name, ContentId(String::from("coucou"))); assert_eq!(received.creator, Creator::Initiator); let elem2 = Element::from(received.clone()); let received2 = Received::try_from(elem2).unwrap(); assert_eq!(received2.name, ContentId(String::from("coucou"))); assert_eq!(received2.creator, Creator::Initiator); } #[test] #[cfg_attr(feature = "disable-validation", should_panic = "Result::unwrap_err")] fn test_received_unknown_child() { let elem: Element = "".parse().unwrap(); let error = Received::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!(message, "Unknown child in Received element."); } #[test] fn test_received_missing_name() { let elem: Element = "" .parse() .unwrap(); let error = Received::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!( message, "Required attribute field 'name' on Received element missing." ); } #[test] fn test_received_invalid_creator() { let elem: Element = "".parse().unwrap(); let error = Received::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::TextParseError(string)) => string, _ => panic!(), }; assert_eq!( message.to_string(), "Unknown value for 'creator' attribute." ); } #[cfg(not(feature = "disable-validation"))] #[test] fn test_invalid_received() { let elem: Element = "".parse().unwrap(); let error = Received::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!(message, "Unknown attribute in Received element."); } #[test] fn test_checksum_valid() { let elem: Element = "w0mcJylzCn+AfvuGdqkty2+KP48=".parse().unwrap(); let hash = vec![ 195, 73, 156, 39, 41, 115, 10, 127, 128, 126, 251, 134, 118, 169, 45, 203, 111, 138, 63, 143, ]; let checksum = Checksum::try_from(elem).unwrap(); assert_eq!(checksum.name, ContentId(String::from("coucou"))); assert_eq!(checksum.creator, Creator::Initiator); assert_eq!( checksum.file.hashes, vec!(Hash { algo: Algo::Sha_1, hash: hash.clone() }) ); let elem2 = Element::from(checksum); let checksum2 = Checksum::try_from(elem2).unwrap(); assert_eq!(checksum2.name, ContentId(String::from("coucou"))); assert_eq!(checksum2.creator, Creator::Initiator); assert_eq!( checksum2.file.hashes, vec!(Hash { algo: Algo::Sha_1, hash: hash.clone() }) ); } #[test] #[cfg_attr(feature = "disable-validation", should_panic = "Result::unwrap_err")] fn test_checksum_unknown_child() { let elem: Element = "w0mcJylzCn+AfvuGdqkty2+KP48=".parse().unwrap(); let error = Checksum::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, other => panic!("unexpected error: {:?}", other), }; assert_eq!(message, "Unknown child in Checksum element."); } #[test] fn test_checksum_missing_name() { let elem: Element = "w0mcJylzCn+AfvuGdqkty2+KP48=".parse().unwrap(); let error = Checksum::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!( message, "Required attribute field 'name' on Checksum element missing." ); } #[test] fn test_checksum_invalid_creator() { let elem: Element = "w0mcJylzCn+AfvuGdqkty2+KP48=".parse().unwrap(); let error = Checksum::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::TextParseError(string)) => string, _ => panic!(), }; assert_eq!( message.to_string(), "Unknown value for 'creator' attribute." ); } #[cfg(not(feature = "disable-validation"))] #[test] fn test_invalid_checksum() { let elem: Element = "w0mcJylzCn+AfvuGdqkty2+KP48=".parse().unwrap(); let error = Checksum::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!(message, "Unknown attribute in Checksum element."); } #[test] fn test_range() { let elem: Element = "" .parse() .unwrap(); let range = Range::try_from(elem).unwrap(); assert_eq!(range.offset, 0); assert_eq!(range.length, None); assert_eq!(range.hashes, vec!()); let elem: Element = "kHp5RSzW/h7Gm1etSf90Mr5PC/k=".parse().unwrap(); let hashes = vec![Hash { algo: Algo::Sha_1, hash: vec![ 144, 122, 121, 69, 44, 214, 254, 30, 198, 155, 87, 173, 73, 255, 116, 50, 190, 79, 11, 249, ], }]; let range = Range::try_from(elem).unwrap(); assert_eq!(range.offset, 2048); assert_eq!(range.length, Some(1024)); assert_eq!(range.hashes, hashes); let elem2 = Element::from(range); let range2 = Range::try_from(elem2).unwrap(); assert_eq!(range2.offset, 2048); assert_eq!(range2.length, Some(1024)); assert_eq!(range2.hashes, hashes); } #[cfg(not(feature = "disable-validation"))] #[test] fn test_invalid_range() { let elem: Element = "" .parse() .unwrap(); let error = Range::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!(message, "Unknown attribute in Range element."); } } xmpp-parsers-0.22.0/src/jingle_grouping.rs000064400000000000000000000045441046102023000166740ustar 00000000000000// Copyright (c) 2020 Emmanuel Gil Peyrot // // 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 xso::{AsXml, FromXml}; use crate::jingle::ContentId; use crate::ns; generate_attribute!( /// The semantics of the grouping. Semantics, "semantics", { /// Lip synchronsation. Ls => "LS", /// Bundle. Bundle => "BUNDLE", } ); /// Describes a content that should be grouped with other ones. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::JINGLE_GROUPING, name = "content")] pub struct Content { /// The name of the matching [`Content`](crate::jingle::Content). #[xml(attribute)] pub name: ContentId, } impl Content { /// Creates a new \ element. pub fn new(name: &str) -> Content { Content { name: ContentId(name.to_string()), } } } /// A semantic group of contents. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::JINGLE_GROUPING, name = "group")] pub struct Group { /// Semantics of the grouping. #[xml(attribute)] pub semantics: Semantics, /// List of contents that should be grouped with each other. #[xml(child(n = ..))] pub contents: Vec, } #[cfg(test)] mod tests { use super::*; use minidom::Element; #[cfg(target_pointer_width = "32")] #[test] fn test_size() { assert_size!(Semantics, 1); assert_size!(Content, 12); assert_size!(Group, 16); } #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(Semantics, 1); assert_size!(Content, 24); assert_size!(Group, 32); } #[test] fn parse_group() { let elem: Element = " " .parse() .unwrap(); let group = Group::try_from(elem).unwrap(); assert_eq!(group.semantics, Semantics::Bundle); assert_eq!(group.contents.len(), 2); assert_eq!( group.contents, &[Content::new("voice"), Content::new("webcam")] ); } } xmpp-parsers-0.22.0/src/jingle_ibb.rs000064400000000000000000000104231046102023000155670ustar 00000000000000// Copyright (c) 2017 Emmanuel Gil Peyrot // // 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 xso::{AsXml, FromXml}; use crate::ibb::{Stanza, StreamId}; use crate::ns; /// Describes an [In-Band Bytestream](https://xmpp.org/extensions/xep-0047.html) /// Jingle transport, see also the [IBB module](../ibb.rs). #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::JINGLE_IBB, name = "transport")] pub struct Transport { /// Maximum size in bytes for each chunk. #[xml(attribute(name = "block-size"))] pub block_size: u16, /// The identifier to be used to create a stream. #[xml(attribute)] pub sid: StreamId, /// Which stanza type to use to exchange data. #[xml(attribute(default))] pub stanza: Stanza, } #[cfg(test)] mod tests { use super::*; use minidom::Element; use xso::error::{Error, FromElementError}; #[cfg(target_pointer_width = "32")] #[test] fn test_size() { assert_size!(Transport, 16); } #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(Transport, 32); } #[test] fn test_simple() { let elem: Element = "" .parse() .unwrap(); let transport = Transport::try_from(elem).unwrap(); assert_eq!(transport.block_size, 3); assert_eq!(transport.sid, StreamId(String::from("coucou"))); assert_eq!(transport.stanza, Stanza::Iq); } #[test] fn test_invalid() { let elem: Element = "" .parse() .unwrap(); let error = Transport::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!( message, "Required attribute field 'block_size' on Transport element missing." ); let elem: Element = "" .parse() .unwrap(); let error = Transport::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::TextParseError(error)) if error.is::() => { error } _ => panic!(), }; assert_eq!( message.to_string(), "number too large to fit in target type" ); let elem: Element = "" .parse() .unwrap(); let error = Transport::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::TextParseError(error)) if error.is::() => { error } _ => panic!(), }; assert_eq!(message.to_string(), "invalid digit found in string"); let elem: Element = "" .parse() .unwrap(); let error = Transport::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!( message, "Required attribute field 'sid' on Transport element missing." ); } #[test] fn test_invalid_stanza() { let elem: Element = "".parse().unwrap(); let error = Transport::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::TextParseError(string)) => string, _ => panic!(), }; assert_eq!(message.to_string(), "Unknown value for 'stanza' attribute."); } } xmpp-parsers-0.22.0/src/jingle_ice_udp.rs000064400000000000000000000266221046102023000164530ustar 00000000000000// Copyright (c) 2019 Emmanuel Gil Peyrot // // 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 core::net::IpAddr; use xso::{AsXml, FromXml}; use crate::jingle_dtls_srtp::Fingerprint; use crate::ns; /// Wrapper element for an ICE-UDP transport. #[derive(FromXml, AsXml, Debug, PartialEq, Clone, Default)] #[xml(namespace = ns::JINGLE_ICE_UDP, name = "transport")] pub struct Transport { /// A Password as defined in ICE-CORE. #[xml(attribute(default))] pwd: Option, /// A User Fragment as defined in ICE-CORE. #[xml(attribute(default))] ufrag: Option, /// List of candidates for this ICE-UDP session. #[xml(child(n = ..))] candidates: Vec, /// Fingerprint of the key used for the DTLS handshake. #[xml(child(default))] fingerprint: Option, } impl Transport { /// Create a new ICE-UDP transport. pub fn new() -> Transport { Transport::default() } /// Add a candidate to this transport. pub fn add_candidate(mut self, candidate: Candidate) -> Self { self.candidates.push(candidate); self } /// Set the DTLS-SRTP fingerprint of this transport. pub fn with_fingerprint(mut self, fingerprint: Fingerprint) -> Self { self.fingerprint = Some(fingerprint); self } } generate_attribute!( /// A Candidate Type as defined in ICE-CORE. Type, "type", { /// Host candidate. Host => "host", /// Peer reflexive candidate. Prflx => "prflx", /// Relayed candidate. Relay => "relay", /// Server reflexive candidate. Srflx => "srflx", } ); /// A candidate for an ICE-UDP session. #[derive(FromXml, AsXml, Debug, PartialEq, Clone)] #[xml(namespace = ns::JINGLE_ICE_UDP, name = "candidate")] pub struct Candidate { /// A Component ID as defined in ICE-CORE. #[xml(attribute)] pub component: u8, /// A Foundation as defined in ICE-CORE. #[xml(attribute)] pub foundation: String, /// An index, starting at 0, that enables the parties to keep track of updates to the /// candidate throughout the life of the session. #[xml(attribute)] pub generation: u8, /// A unique identifier for the candidate. #[xml(attribute)] pub id: String, /// The Internet Protocol (IP) address for the candidate transport mechanism; this can be /// either an IPv4 address or an IPv6 address. #[xml(attribute)] pub ip: IpAddr, /// The port at the candidate IP address. #[xml(attribute)] pub port: u16, /// A Priority as defined in ICE-CORE. #[xml(attribute)] pub priority: u32, /// The protocol to be used. The only value defined by this specification is "udp". #[xml(attribute)] pub protocol: String, /// A related address as defined in ICE-CORE. #[xml(attribute(default, name = "rel-addr"))] pub rel_addr: Option, /// A related port as defined in ICE-CORE. #[xml(attribute(default, name = "rel-port"))] pub rel_port: Option, /// An index, starting at 0, referencing which network this candidate is on for a given /// peer. #[xml(attribute(default))] pub network: Option, /// A Candidate Type as defined in ICE-CORE. #[xml(attribute(name = "type"))] pub type_: Type, } #[cfg(test)] mod tests { use super::*; use crate::hashes::Algo; use crate::jingle_dtls_srtp::Setup; use minidom::Element; #[cfg(target_pointer_width = "32")] #[test] fn test_size() { assert_size!(Transport, 64); assert_size!(Type, 1); assert_size!(Candidate, 88); } #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(Transport, 128); assert_size!(Type, 1); assert_size!(Candidate, 128); } #[test] fn test_gajim() { let elem: Element = " " .parse() .unwrap(); let transport = Transport::try_from(elem).unwrap(); assert_eq!(transport.pwd.unwrap(), "wakMJ8Ydd5rqnPaFerws5o"); assert_eq!(transport.ufrag.unwrap(), "aeXX"); } #[test] fn test_jitsi_meet() { let elem: Element = " 97:F2:B5:BE:DB:A6:00:B1:3E:40:B2:41:3C:0D:FC:E0:BD:B2:A0:E8 " .parse() .unwrap(); let transport = Transport::try_from(elem).unwrap(); assert_eq!(transport.pwd.unwrap(), "7lk9uul39gckit6t02oavv2r9j"); assert_eq!(transport.ufrag.unwrap(), "2acq51d4p07v2m"); let fingerprint = transport.fingerprint.unwrap(); assert_eq!(fingerprint.hash, Algo::Sha_1); assert_eq!(fingerprint.setup, Setup::Actpass); assert_eq!( fingerprint.value, [ 151, 242, 181, 190, 219, 166, 0, 177, 62, 64, 178, 65, 60, 13, 252, 224, 189, 178, 160, 232 ] ); } #[test] fn test_serialize_transport() { let reference: Element = "02:1A:CC:54:27:AB:EB:9C:53:3F:3E:4B:65:2E:7D:46:3F:54:42:CD:54:F1:7A:03:A2:7D:F9:B0:7F:46:19:B2" .parse() .unwrap(); let elem: Element = "02:1A:CC:54:27:AB:EB:9C:53:3F:3E:4B:65:2E:7D:46:3F:54:42:CD:54:F1:7A:03:A2:7D:F9:B0:7F:46:19:B2" .parse() .unwrap(); let fingerprint = Fingerprint::try_from(elem).unwrap(); let transport = Transport { pwd: None, ufrag: None, candidates: vec![], fingerprint: Some(fingerprint), }; let serialized: Element = transport.into(); assert_eq!(serialized, reference); } } xmpp-parsers-0.22.0/src/jingle_message.rs000064400000000000000000000051271046102023000164640ustar 00000000000000// Copyright (c) 2017 Emmanuel Gil Peyrot // // 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 xso::{AsXml, FromXml}; use crate::jingle::SessionId; use crate::ns; use minidom::Element; /// Defines a protocol for broadcasting Jingle requests to all of the clients /// of a user. #[derive(FromXml, AsXml, Debug, Clone)] #[xml(namespace = ns::JINGLE_MESSAGE, exhaustive)] pub enum JingleMI { /// Indicates we want to start a Jingle session. #[xml(name = "propose")] Propose { /// The generated session identifier, must be unique between two users. #[xml(attribute = "id")] sid: SessionId, /// The application description of the proposed session. // TODO: Use a more specialised type here. #[xml(element)] description: Element, }, /// Cancels a previously proposed session. #[xml(name = "retract")] Retract(#[xml(attribute = "id")] SessionId), /// Accepts a session proposed by the other party. #[xml(name = "accept")] Accept(#[xml(attribute = "id")] SessionId), /// Proceed with a previously proposed session. #[xml(name = "proceed")] Proceed(#[xml(attribute = "id")] SessionId), /// Rejects a session proposed by the other party. #[xml(name = "reject")] Reject(#[xml(attribute = "id")] SessionId), } #[cfg(test)] mod tests { use super::*; use xso::error::{Error, FromElementError}; #[cfg(target_pointer_width = "32")] #[test] fn test_size() { assert_size!(JingleMI, 72); } #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(JingleMI, 144); } #[test] fn test_simple() { let elem: Element = "" .parse() .unwrap(); JingleMI::try_from(elem).unwrap(); } // TODO: Enable this test again once #[xml(element)] supports filtering on the element name. #[test] #[ignore] fn test_invalid_child() { let elem: Element = "" .parse() .unwrap(); let error = JingleMI::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!(message, "Unknown child in propose element."); } } xmpp-parsers-0.22.0/src/jingle_raw_udp.rs000064400000000000000000000060601046102023000164760ustar 00000000000000// Copyright (c) 2020 Emmanuel Gil Peyrot // // 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 core::net::IpAddr; use xso::{AsXml, FromXml}; use crate::jingle_ice_udp::Type; use crate::ns; /// Wrapper element for an raw UDP transport. #[derive(FromXml, AsXml, PartialEq, Debug, Clone, Default)] #[xml(namespace = ns::JINGLE_RAW_UDP, name = "transport")] pub struct Transport { /// List of candidates for this raw UDP session. #[xml(child(n = ..))] pub candidates: Vec, } impl Transport { /// Create a new ICE-UDP transport. pub fn new() -> Transport { Transport::default() } /// Add a candidate to this transport. pub fn add_candidate(mut self, candidate: Candidate) -> Self { self.candidates.push(candidate); self } } /// A candidate for an ICE-UDP session. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::JINGLE_RAW_UDP, name = "candidate")] pub struct Candidate { /// A Component ID as defined in ICE-CORE. #[xml(attribute)] pub component: u8, /// An index, starting at 0, that enables the parties to keep track of updates to the /// candidate throughout the life of the session. #[xml(attribute)] pub generation: u8, /// A unique identifier for the candidate. #[xml(attribute)] pub id: String, /// The Internet Protocol (IP) address for the candidate transport mechanism; this can be /// either an IPv4 address or an IPv6 address. #[xml(attribute)] pub ip: IpAddr, /// The port at the candidate IP address. #[xml(attribute)] pub port: u16, /// A Candidate Type as defined in ICE-CORE. #[xml(attribute(default, name = "type"))] pub type_: Option, } #[cfg(test)] mod tests { use super::*; use minidom::Element; #[cfg(target_pointer_width = "32")] #[test] fn test_size() { assert_size!(Transport, 12); assert_size!(Candidate, 36); } #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(Transport, 24); assert_size!(Candidate, 48); } #[test] fn example_1() { let elem: Element = " " .parse() .unwrap(); let mut transport = Transport::try_from(elem).unwrap(); assert_eq!(transport.candidates.len(), 1); let candidate = transport.candidates.pop().unwrap(); assert_eq!(candidate.component, 1); assert_eq!(candidate.generation, 0); assert_eq!(candidate.id, "a9j3mnbtu1"); assert_eq!(candidate.ip, "10.1.1.104".parse::().unwrap()); assert_eq!(candidate.port, 13540u16); assert!(candidate.type_.is_none()); } } xmpp-parsers-0.22.0/src/jingle_rtcp_fb.rs000064400000000000000000000025011046102023000164500ustar 00000000000000// Copyright (c) 2019 Emmanuel Gil Peyrot // // 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 xso::{AsXml, FromXml}; use crate::ns; /// Wrapper element for a rtcp-fb. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::JINGLE_RTCP_FB, name = "rtcp-fb")] pub struct RtcpFb { /// Type of this rtcp-fb. #[xml(attribute = "type")] pub type_: String, /// Subtype of this rtcp-fb, if relevant. #[xml(attribute(default))] pub subtype: Option, } #[cfg(test)] mod tests { use super::*; use minidom::Element; #[cfg(target_pointer_width = "32")] #[test] fn test_size() { assert_size!(RtcpFb, 24); } #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(RtcpFb, 48); } #[test] fn parse_simple() { let elem: Element = "" .parse() .unwrap(); let rtcp_fb = RtcpFb::try_from(elem).unwrap(); assert_eq!(rtcp_fb.type_, "nack"); assert_eq!(rtcp_fb.subtype.unwrap(), "sli"); } } xmpp-parsers-0.22.0/src/jingle_rtp.rs000064400000000000000000000162611046102023000156460ustar 00000000000000// Copyright (c) 2019-2020 Emmanuel Gil Peyrot // // 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 xso::{AsXml, FromXml}; use crate::jingle_rtcp_fb::RtcpFb; use crate::jingle_rtp_hdrext::RtpHdrext; use crate::jingle_ssma::{Group, Source}; use crate::ns; /// Specifies the ability to multiplex RTP Data and Control Packets on a single port as /// described in RFC 5761. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::JINGLE_RTP, name = "rtcp-mux")] pub struct RtcpMux; /// Wrapper element describing an RTP session. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::JINGLE_RTP, name = "description")] pub struct Description { /// Namespace of the encryption scheme used. #[xml(attribute)] pub media: String, /// User-friendly name for the encryption scheme, should be `None` for OTR, /// legacy OpenPGP and OX. // XXX: is this a String or an u32?! Refer to RFC 3550. #[xml(attribute(default))] pub ssrc: Option, /// List of encodings that can be used for this RTP stream. #[xml(child(n = ..))] pub payload_types: Vec, /// Specifies the ability to multiplex RTP Data and Control Packets on a single port as /// described in RFC 5761. #[xml(child(default))] pub rtcp_mux: Option, /// List of ssrc-group. #[xml(child(n = ..))] pub ssrc_groups: Vec, /// List of ssrc. #[xml(child(n = ..))] pub ssrcs: Vec, /// List of header extensions. #[xml(child(n = ..))] pub hdrexts: Vec, // TODO: Add support for and . } impl Description { /// Create a new RTP description. pub fn new(media: String) -> Description { Description { media, ssrc: None, payload_types: Vec::new(), rtcp_mux: None, ssrc_groups: Vec::new(), ssrcs: Vec::new(), hdrexts: Vec::new(), } } } generate_attribute!( /// The number of channels. Channels, "channels", u8, Default = 1 ); /// An encoding that can be used for an RTP stream. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::JINGLE_RTP, name = "payload-type")] pub struct PayloadType { /// The number of channels. #[xml(attribute(default))] pub channels: Channels, /// The sampling frequency in Hertz. #[xml(attribute(default))] pub clockrate: Option, /// The payload identifier. #[xml(attribute)] pub id: u8, /// Maximum packet time as specified in RFC 4566. #[xml(attribute(default))] pub maxptime: Option, /// The appropriate subtype of the MIME type. #[xml(attribute(default))] pub name: Option, /// Packet time as specified in RFC 4566. #[xml(attribute(default))] pub ptime: Option, /// List of parameters specifying this payload-type. /// /// Their order MUST be ignored. #[xml(child(n = ..))] pub parameters: Vec, /// List of rtcp-fb parameters from XEP-0293. #[xml(child(n = ..))] pub rtcp_fbs: Vec, } impl PayloadType { /// Create a new RTP payload-type. pub fn new(id: u8, name: String, clockrate: u32, channels: u8) -> PayloadType { PayloadType { channels: Channels(channels), clockrate: Some(clockrate), id, maxptime: None, name: Some(name), ptime: None, parameters: Vec::new(), rtcp_fbs: Vec::new(), } } /// Create a new RTP payload-type without a clockrate. Warning: this is invalid as per /// RFC 4566! pub fn without_clockrate(id: u8, name: String) -> PayloadType { PayloadType { channels: Default::default(), clockrate: None, id, maxptime: None, name: Some(name), ptime: None, parameters: Vec::new(), rtcp_fbs: Vec::new(), } } } /// Parameter related to a payload. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::JINGLE_RTP, name = "parameter")] pub struct Parameter { /// The name of the parameter, from the list at /// #[xml(attribute)] pub name: String, /// The value of this parameter. #[xml(attribute)] pub value: String, } #[cfg(test)] mod tests { use super::*; use minidom::Element; #[cfg(target_pointer_width = "32")] #[test] fn test_size() { assert_size!(Description, 76); assert_size!(Channels, 1); assert_size!(PayloadType, 64); assert_size!(Parameter, 24); } #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(Description, 152); assert_size!(Channels, 1); assert_size!(PayloadType, 104); assert_size!(Parameter, 48); } #[test] fn test_simple() { let elem: Element = " " .parse() .unwrap(); let desc = Description::try_from(elem).unwrap(); assert_eq!(desc.media, "audio"); assert_eq!(desc.ssrc, None); } } xmpp-parsers-0.22.0/src/jingle_rtp_hdrext.rs000064400000000000000000000050041046102023000172150ustar 00000000000000// Copyright (c) 2020 Emmanuel Gil Peyrot // // 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 xso::{AsXml, FromXml}; use crate::ns; generate_attribute!( /// Which party is allowed to send the negotiated RTP Header Extensions. Senders, "senders", { /// Both parties can send them. Both => "both", /// Only the initiator can send them. Initiator => "initiator", /// Only the responder can send them. Responder => "responder", }, Default = Both ); /// Header extensions to be used in a RTP description. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::JINGLE_RTP_HDREXT, name = "rtp-hdrext")] pub struct RtpHdrext { /// The ID of the extensions. The allowed values are only in the 1-256, 4096-4351 ranges, /// this isn’t enforced by xmpp-parsers yet! // TODO: make it so. #[xml(attribute)] pub id: u16, /// The URI that defines the extension. #[xml(attribute)] pub uri: String, /// Which party is allowed to send the negotiated RTP Header Extensions. #[xml(attribute(default))] pub senders: Senders, } impl RtpHdrext { /// Create a new RTP header extension element. pub fn new(id: u16, uri: String) -> RtpHdrext { RtpHdrext { id, uri, senders: Default::default(), } } /// Set the senders. pub fn with_senders(mut self, senders: Senders) -> RtpHdrext { self.senders = senders; self } } #[cfg(test)] mod tests { use super::*; use minidom::Element; #[cfg(target_pointer_width = "32")] #[test] fn test_size() { assert_size!(Senders, 1); assert_size!(RtpHdrext, 16); } #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(Senders, 1); assert_size!(RtpHdrext, 32); } #[test] fn parse_exthdr() { let elem: Element = "" .parse() .unwrap(); let rtp_hdrext = RtpHdrext::try_from(elem).unwrap(); assert_eq!(rtp_hdrext.id, 1); assert_eq!(rtp_hdrext.uri, "urn:ietf:params:rtp-hdrext:toffset"); assert_eq!(rtp_hdrext.senders, Senders::Both); } } xmpp-parsers-0.22.0/src/jingle_s5b.rs000064400000000000000000000305231046102023000155270ustar 00000000000000// Copyright (c) 2017 Emmanuel Gil Peyrot // // 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 xso::exports::rxml; use xso::{ error::{Error, FromElementError}, AsXml, FromXml, }; use crate::ns; use core::net::IpAddr; use jid::Jid; use minidom::Element; generate_attribute!( /// The type of the connection being proposed by this candidate. Type, "type", { /// Direct connection using NAT assisting technologies like NAT-PMP or /// UPnP-IGD. Assisted => "assisted", /// Direct connection using the given interface. Direct => "direct", /// SOCKS5 relay. Proxy => "proxy", /// Tunnel protocol such as Teredo. Tunnel => "tunnel", }, Default = Direct ); generate_attribute!( /// Which mode to use for the connection. Mode, "mode", { /// Use TCP, which is the default. Tcp => "tcp", /// Use UDP. Udp => "udp", }, Default = Tcp ); generate_id!( /// An identifier for a candidate. CandidateId ); generate_id!( /// An identifier for a stream. StreamId ); /// A candidate for a connection. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::JINGLE_S5B, name = "candidate")] pub struct Candidate { /// The identifier for this candidate. #[xml(attribute)] cid: CandidateId, /// The host to connect to. #[xml(attribute)] host: IpAddr, /// The JID to request at the given end. #[xml(attribute)] jid: Jid, /// The port to connect to. #[xml(attribute(default))] port: Option, /// The priority of this candidate, computed using this formula: /// priority = (2^16)*(type preference) + (local preference) #[xml(attribute)] priority: u32, /// The type of the connection being proposed by this candidate. #[xml(attribute(default, name = "type"))] type_: Type, } impl Candidate { /// Creates a new candidate with the given parameters. pub fn new(cid: CandidateId, host: IpAddr, jid: Jid, priority: u32) -> Candidate { Candidate { cid, host, jid, priority, port: Default::default(), type_: Default::default(), } } /// Sets the port of this candidate. pub fn with_port(mut self, port: u16) -> Candidate { self.port = Some(port); self } /// Sets the type of this candidate. pub fn with_type(mut self, type_: Type) -> Candidate { self.type_ = type_; self } } /// The payload of a transport. #[derive(Debug, Clone, PartialEq)] pub enum TransportPayload { /// The responder informs the initiator that the bytestream pointed by this /// candidate has been activated. Activated(CandidateId), /// A list of suggested candidates. Candidates(Vec), /// Both parties failed to use a candidate, they should fallback to another /// transport. CandidateError, /// The candidate pointed here should be used by both parties. CandidateUsed(CandidateId), /// This entity can’t connect to the SOCKS5 proxy. ProxyError, /// XXX: Invalid, should not be found in the wild. None, } /// Describes a Jingle transport using a direct or proxied connection. #[derive(Debug, Clone, PartialEq)] pub struct Transport { /// The stream identifier for this transport. pub sid: StreamId, /// The destination address. pub dstaddr: Option, /// The mode to be used for the transfer. pub mode: Mode, /// The payload of this transport. pub payload: TransportPayload, } impl Transport { /// Creates a new transport element. pub fn new(sid: StreamId) -> Transport { Transport { sid, dstaddr: None, mode: Default::default(), payload: TransportPayload::None, } } /// Sets the destination address of this transport. pub fn with_dstaddr(mut self, dstaddr: String) -> Transport { self.dstaddr = Some(dstaddr); self } /// Sets the mode of this transport. pub fn with_mode(mut self, mode: Mode) -> Transport { self.mode = mode; self } /// Sets the payload of this transport. pub fn with_payload(mut self, payload: TransportPayload) -> Transport { self.payload = payload; self } } impl TryFrom for Transport { type Error = FromElementError; fn try_from(elem: Element) -> Result { check_self!(elem, "transport", JINGLE_S5B); check_no_unknown_attributes!(elem, "transport", ["sid", "dstaddr", "mode"]); let sid = get_attr!(elem, "sid", Required); let dstaddr = get_attr!(elem, "dstaddr", Option); let mode = get_attr!(elem, "mode", Default); let mut payload = None; for child in elem.children() { payload = Some(if child.is("candidate", ns::JINGLE_S5B) { let mut candidates = match payload { Some(TransportPayload::Candidates(candidates)) => candidates, Some(_) => return Err(Error::Other( "Non-candidate child already present in JingleS5B transport element.", ) .into()), None => vec![], }; candidates.push(Candidate::try_from(child.clone())?); TransportPayload::Candidates(candidates) } else if child.is("activated", ns::JINGLE_S5B) { if payload.is_some() { return Err(Error::Other( "Non-activated child already present in JingleS5B transport element.", ) .into()); } let cid = get_attr!(child, "cid", Required); TransportPayload::Activated(cid) } else if child.is("candidate-error", ns::JINGLE_S5B) { if payload.is_some() { return Err(Error::Other( "Non-candidate-error child already present in JingleS5B transport element.", ) .into()); } TransportPayload::CandidateError } else if child.is("candidate-used", ns::JINGLE_S5B) { if payload.is_some() { return Err(Error::Other( "Non-candidate-used child already present in JingleS5B transport element.", ) .into()); } let cid = get_attr!(child, "cid", Required); TransportPayload::CandidateUsed(cid) } else if child.is("proxy-error", ns::JINGLE_S5B) { if payload.is_some() { return Err(Error::Other( "Non-proxy-error child already present in JingleS5B transport element.", ) .into()); } TransportPayload::ProxyError } else { return Err(Error::Other("Unknown child in JingleS5B transport element.").into()); }); } let payload = payload.unwrap_or(TransportPayload::None); Ok(Transport { sid, dstaddr, mode, payload, }) } } impl From for Element { fn from(transport: Transport) -> Element { Element::builder("transport", ns::JINGLE_S5B) .attr(rxml::xml_ncname!("sid").into(), transport.sid) .attr(rxml::xml_ncname!("dstaddr").into(), transport.dstaddr) .attr(rxml::xml_ncname!("mode").into(), transport.mode) .append_all(match transport.payload { TransportPayload::Candidates(candidates) => candidates .into_iter() .map(Element::from) .collect::>(), TransportPayload::Activated(cid) => { vec![Element::builder("activated", ns::JINGLE_S5B) .attr(rxml::xml_ncname!("cid").into(), cid) .build()] } TransportPayload::CandidateError => { vec![Element::builder("candidate-error", ns::JINGLE_S5B).build()] } TransportPayload::CandidateUsed(cid) => { vec![Element::builder("candidate-used", ns::JINGLE_S5B) .attr(rxml::xml_ncname!("cid").into(), cid) .build()] } TransportPayload::ProxyError => { vec![Element::builder("proxy-error", ns::JINGLE_S5B).build()] } TransportPayload::None => vec![], }) .build() } } impl ::xso::FromXml for Transport { type Builder = ::xso::minidom_compat::FromEventsViaElement; fn from_events( qname: ::xso::exports::rxml::QName, attrs: ::xso::exports::rxml::AttrMap, _ctx: &::xso::Context<'_>, ) -> Result { if qname.0 != crate::ns::JINGLE_S5B || qname.1 != "transport" { return Err(::xso::error::FromEventsError::Mismatch { name: qname, attrs }); } Self::Builder::new(qname, attrs) } } impl ::xso::AsXml for Transport { type ItemIter<'x> = ::xso::minidom_compat::AsItemsViaElement<'x>; fn as_xml_iter(&self) -> Result, ::xso::error::Error> { ::xso::minidom_compat::AsItemsViaElement::new(self.clone()) } } #[cfg(test)] mod tests { use super::*; use core::str::FromStr; #[cfg(target_pointer_width = "32")] #[test] fn test_size() { assert_size!(Type, 1); assert_size!(Mode, 1); assert_size!(CandidateId, 12); assert_size!(StreamId, 12); assert_size!(Candidate, 56); assert_size!(TransportPayload, 16); assert_size!(Transport, 44); } #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(Type, 1); assert_size!(Mode, 1); assert_size!(CandidateId, 24); assert_size!(StreamId, 24); assert_size!(Candidate, 88); assert_size!(TransportPayload, 32); assert_size!(Transport, 88); } #[test] fn test_simple() { let elem: Element = "" .parse() .unwrap(); let transport = Transport::try_from(elem).unwrap(); assert_eq!(transport.sid, StreamId(String::from("coucou"))); assert_eq!(transport.dstaddr, None); assert_eq!(transport.mode, Mode::Tcp); match transport.payload { TransportPayload::None => (), _ => panic!("Wrong element inside transport!"), } } #[test] fn test_serialise_activated() { let elem: Element = "".parse().unwrap(); let transport = Transport { sid: StreamId(String::from("coucou")), dstaddr: None, mode: Mode::Tcp, payload: TransportPayload::Activated(CandidateId(String::from("coucou"))), }; let elem2: Element = transport.into(); assert_eq!(elem, elem2); } #[test] fn test_serialise_candidate() { let elem: Element = "".parse().unwrap(); let transport = Transport { sid: StreamId(String::from("coucou")), dstaddr: None, mode: Mode::Tcp, payload: TransportPayload::Candidates(vec![Candidate { cid: CandidateId(String::from("coucou")), host: IpAddr::from_str("127.0.0.1").unwrap(), jid: Jid::new("coucou@coucou").unwrap(), port: None, priority: 0u32, type_: Type::Direct, }]), }; let elem2: Element = transport.into(); assert_eq!(elem, elem2); } } xmpp-parsers-0.22.0/src/jingle_ssma.rs000064400000000000000000000102141046102023000157740ustar 00000000000000// Copyright (c) 2019 Emmanuel Gil Peyrot // // 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 xso::{AsXml, FromXml}; use crate::ns; /// Source element for the ssrc SDP attribute. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::JINGLE_SSMA, name = "source")] pub struct Source { /// Maps to the ssrc-id parameter. #[xml(attribute = "ssrc")] pub id: u32, /// List of attributes for this source. #[xml(child(n = ..))] pub parameters: Vec, } impl Source { /// Create a new SSMA Source element. pub fn new(id: u32) -> Source { Source { id, parameters: Vec::new(), } } } /// Parameter associated with a ssrc. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::JINGLE_SSMA, name = "parameter")] pub struct Parameter { /// The name of the parameter. #[xml(attribute)] pub name: String, /// The optional value of the parameter. #[xml(attribute(default))] pub value: Option, } generate_attribute!( /// From RFC5888, the list of allowed semantics. Semantics, "semantics", { /// Lip Synchronization, defined in RFC5888. Ls => "LS", /// Flow Identification, defined in RFC5888. Fid => "FID", /// Single Reservation Flow, defined in RFC3524. Srf => "SRF", /// Alternative Network Address Types, defined in RFC4091. Anat => "ANAT", /// Forward Error Correction, defined in RFC4756. Fec => "FEC", /// Decoding Dependency, defined in RFC5583. Ddp => "DDP", } ); /// Element grouping multiple ssrc. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::JINGLE_SSMA, name = "ssrc-group")] pub struct Group { /// The semantics of this group. #[xml(attribute)] pub semantics: Semantics, /// The various ssrc concerned by this group. #[xml(child(n = ..))] pub sources: Vec, } #[cfg(test)] mod tests { use super::*; use minidom::Element; #[cfg(target_pointer_width = "32")] #[test] fn test_size() { assert_size!(Source, 16); assert_size!(Parameter, 24); assert_size!(Semantics, 1); assert_size!(Group, 16); } #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(Source, 32); assert_size!(Parameter, 48); assert_size!(Semantics, 1); assert_size!(Group, 32); } #[test] fn parse_source() { let elem: Element = " " .parse() .unwrap(); let mut ssrc = Source::try_from(elem).unwrap(); assert_eq!(ssrc.id, 1656081975); assert_eq!(ssrc.parameters.len(), 2); let parameter = ssrc.parameters.pop().unwrap(); assert_eq!(parameter.name, "msid"); assert_eq!( parameter.value.unwrap(), "MLTJKIHilGn71fNQoszkQ4jlPTuS5vJyKVIv MLTJKIHilGn71fNQoszkQ4jlPTuS5vJyKVIva0" ); let parameter = ssrc.parameters.pop().unwrap(); assert_eq!(parameter.name, "cname"); assert_eq!(parameter.value.unwrap(), "Yv/wvbCdsDW2Prgd"); } #[test] fn parse_source_group() { let elem: Element = " " .parse() .unwrap(); let mut group = Group::try_from(elem).unwrap(); assert_eq!(group.semantics, Semantics::Fid); assert_eq!(group.sources.len(), 2); let source = group.sources.pop().unwrap(); assert_eq!(source.id, 386328120); let source = group.sources.pop().unwrap(); assert_eq!(source.id, 2301230316); } } xmpp-parsers-0.22.0/src/jingle_thumbnails.rs000064400000000000000000000040061046102023000172010ustar 00000000000000// Copyright (c) 2023 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/. //! Jingle thumbnails (XEP-0264) use xso::{AsXml, FromXml}; use crate::ns; use core::num::NonZeroU16; /// A Jingle thumbnail. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::JINGLE_THUMBNAILS, name = "thumbnail")] pub struct Thumbnail { /// The URI of the thumbnail. #[xml(attribute)] pub uri: String, /// The media type of the thumbnail. #[xml(attribute(default, name = "media-type"))] pub media_type: Option, /// The width of the thumbnail. #[xml(attribute(default))] pub width: Option, /// The height of the thumbnail. #[xml(attribute(default))] pub height: Option, } #[cfg(test)] mod tests { use crate::jingle_thumbnails::Thumbnail; use core::num::NonZeroU16; use minidom::Element; #[cfg(target_pointer_width = "32")] #[test] fn test_size() { assert_size!(Thumbnail, 28); } #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(Thumbnail, 56); } #[test] fn test_simple_parse() { // Extracted from https://xmpp.org/extensions/xep-0264.html#example-1 let test_xml = ""; let elem: Element = test_xml.parse().unwrap(); let thumbnail = Thumbnail::try_from(elem).unwrap(); assert_eq!( thumbnail.uri, "cid:sha1+ffd7c8d28e9c5e82afea41f97108c6b4@bob.xmpp.org" ); assert_eq!(thumbnail.media_type.unwrap(), "image/png"); assert_eq!(thumbnail.width, NonZeroU16::new(128)); assert_eq!(thumbnail.height, NonZeroU16::new(96)); } } xmpp-parsers-0.22.0/src/json_containers.rs000064400000000000000000000034071046102023000167050ustar 00000000000000// Copyright (c) 2025 Maxime “pep” Buquet // // 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 crate::ns; use xso::{AsXml, FromXml}; /// Structure representing a `` element. #[derive(FromXml, AsXml, Debug, Clone, PartialEq)] #[xml(namespace = ns::JSON_CONTAINERS, name = "json")] pub struct JsonContainer(#[xml(text)] serde_json::Value); #[cfg(test)] mod tests { use super::*; use minidom::Element; use crate::Error::TextParseError; use xso::error::FromElementError; #[cfg(target_pointer_width = "32")] #[test] fn test_size() { assert_size!(JsonContainer, 16); } #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(JsonContainer, 32); } #[test] fn test_empty() { let elem: Element = "".parse().unwrap(); let error = JsonContainer::try_from(elem.clone()).unwrap_err(); match error { FromElementError::Invalid(TextParseError(err)) => { assert_eq!( err.to_string(), "EOF while parsing a value at line 1 column 0" ); } _ => panic!(), } } #[test] fn test_simple() { let elem: Element = "{\"a\": 1}" .parse() .unwrap(); let result: serde_json::Value = "{\"a\": 1}".parse().unwrap(); match JsonContainer::try_from(elem.clone()) { Ok(json) => assert_eq!(json.0, result), _ => panic!(), }; } } xmpp-parsers-0.22.0/src/legacy_omemo.rs000064400000000000000000000330611046102023000161460ustar 00000000000000// Copyright (c) 2022 Yureka Lilian // // 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 xso::{text::Base64, AsXml, FromXml}; use crate::message::MessagePayload; use crate::ns; use crate::pubsub::PubSubPayload; /// Element of the device list #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::LEGACY_OMEMO, name = "device")] pub struct Device { /// Device id #[xml(attribute)] pub id: u32, } /// A user's device list contains the OMEMO device ids of all the user's /// devicse. These can be used to look up bundles and build a session. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::LEGACY_OMEMO, name = "list")] pub struct DeviceList { /// List of devices #[xml(child(n = ..))] pub devices: Vec, } impl PubSubPayload for DeviceList {} /// SignedPreKey public key /// Part of a device's bundle #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::LEGACY_OMEMO, name = "signedPreKeyPublic")] pub struct SignedPreKeyPublic { /// SignedPreKey id #[xml(attribute(default, name = "signedPreKeyId"))] pub signed_pre_key_id: Option, /// Serialized PublicKey #[xml(text = Base64)] pub data: Vec, } /// SignedPreKey signature /// Part of a device's bundle #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::LEGACY_OMEMO, name = "signedPreKeySignature")] pub struct SignedPreKeySignature { /// Signature bytes #[xml(text = Base64)] pub data: Vec, } /// Part of a device's bundle #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::LEGACY_OMEMO, name = "identityKey")] pub struct IdentityKey { /// Serialized PublicKey #[xml(text = Base64)] pub data: Vec, } /// List of (single use) PreKeys /// Part of a device's bundle #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::LEGACY_OMEMO, name = "prekeys")] pub struct Prekeys { /// List of (single use) PreKeys #[xml(child(n = ..))] pub keys: Vec, } /// PreKey public key /// Part of a device's bundle #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::LEGACY_OMEMO, name = "preKeyPublic")] pub struct PreKeyPublic { /// PreKey id #[xml(attribute = "preKeyId")] pub pre_key_id: u32, /// Serialized PublicKey #[xml(text = Base64)] pub data: Vec, } /// A collection of publicly accessible data that can be used to build a /// session with a device, namely its public IdentityKey, a signed PreKey with /// corresponding signature, and a list of (single use) PreKeys. #[derive(FromXml, AsXml, Debug, PartialEq, Clone)] #[xml(namespace = ns::LEGACY_OMEMO, name = "bundle")] pub struct Bundle { /// SignedPreKey public key #[xml(child(default))] pub signed_pre_key_public: Option, /// SignedPreKey signature #[xml(child(default))] pub signed_pre_key_signature: Option, /// IdentityKey public key #[xml(child(default))] pub identity_key: Option, /// List of (single use) PreKeys #[xml(child(default))] pub prekeys: Option, } impl PubSubPayload for Bundle {} /// The header contains encrypted keys for a message #[derive(FromXml, AsXml, Debug, PartialEq, Clone)] #[xml(namespace = ns::LEGACY_OMEMO, name = "header")] pub struct Header { /// The device id of the sender #[xml(attribute)] pub sid: u32, /// The key that the payload message is encrypted with, separately /// encrypted for each recipient device. #[xml(child(n = ..))] pub keys: Vec, /// IV used for payload encryption #[xml(child)] pub iv: IV, } /// IV used for payload encryption #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::LEGACY_OMEMO, name = "iv")] pub struct IV { /// IV bytes #[xml(text = Base64)] pub data: Vec, } /// Part of the OMEMO element header #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::LEGACY_OMEMO, name = "key")] pub struct Key { /// The device id this key is encrypted for. #[xml(attribute)] pub rid: u32, /// The key element MUST be tagged with a prekey attribute set to true /// if a PreKeySignalMessage is being used. #[xml(attribute(default))] pub prekey: bool, /// The 16 bytes key and the GCM authentication tag concatenated together /// and encrypted using the corresponding long-standing SignalProtocol /// session #[xml(text = Base64)] pub data: Vec, } /// The encrypted message body #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::LEGACY_OMEMO, name = "payload")] pub struct Payload { /// Encrypted with AES-128 in Galois/Counter Mode (GCM) #[xml(text = Base64)] pub data: Vec, } /// An OMEMO element, which can be either a MessageElement (with payload), /// or a KeyTransportElement (without payload). #[derive(FromXml, AsXml, Debug, PartialEq, Clone)] #[xml(namespace = ns::LEGACY_OMEMO, name = "encrypted")] pub struct Encrypted { /// The header contains encrypted keys for a message #[xml(child)] pub header: Header, /// Payload for MessageElement #[xml(child(default))] pub payload: Option, } impl MessagePayload for Encrypted {} #[cfg(test)] mod tests { use super::*; use minidom::Element; #[test] fn parse_bundle() { let elem: Element = r#" BYAbACA15bPn95p7RGC2XbgQyly8aRKS4BaJ+hD8Ybhe sIJVNDZi/NgFsry4OBdM+adyGttLEXbUh/h/5dVOZveMgyVoIdgwBUzq8Wgd2xYTQMioNzwYebTX+9p0h9eujA== BQFd2p/Oq97vAAdLKA09DlcSg0x1xWn260p1jaeyIhAZ BbjHsF5ndtNV8ToRcJTYSNGePgAWsFGkSL6OG7B7LXRe BeWHsbBNx1uer1ia/nW/6tn/OlqHll9itjjUTIvV39x7 BeVr5xPmNErkwK3ocPmv0Nohy3C4PKQBnxMuOqiXotJY "#.parse().unwrap(); let bundle: Bundle = elem.try_into().unwrap(); let bundle2 = Bundle { signed_pre_key_public: Some(SignedPreKeyPublic { signed_pre_key_id: Some(1), data: vec![ 5, 128, 27, 0, 32, 53, 229, 179, 231, 247, 154, 123, 68, 96, 182, 93, 184, 16, 202, 92, 188, 105, 18, 146, 224, 22, 137, 250, 16, 252, 97, 184, 94, ], }), signed_pre_key_signature: Some(SignedPreKeySignature { data: vec![ 176, 130, 85, 52, 54, 98, 252, 216, 5, 178, 188, 184, 56, 23, 76, 249, 167, 114, 26, 219, 75, 17, 118, 212, 135, 248, 127, 229, 213, 78, 102, 247, 140, 131, 37, 104, 33, 216, 48, 5, 76, 234, 241, 104, 29, 219, 22, 19, 64, 200, 168, 55, 60, 24, 121, 180, 215, 251, 218, 116, 135, 215, 174, 140, ], }), identity_key: Some(IdentityKey { data: vec![ 5, 1, 93, 218, 159, 206, 171, 222, 239, 0, 7, 75, 40, 13, 61, 14, 87, 18, 131, 76, 117, 197, 105, 246, 235, 74, 117, 141, 167, 178, 34, 16, 25, ], }), prekeys: Some(Prekeys { keys: vec![ PreKeyPublic { pre_key_id: 1, data: vec![ 5, 184, 199, 176, 94, 103, 118, 211, 85, 241, 58, 17, 112, 148, 216, 72, 209, 158, 62, 0, 22, 176, 81, 164, 72, 190, 142, 27, 176, 123, 45, 116, 94, ], }, PreKeyPublic { pre_key_id: 2, data: vec![ 5, 229, 135, 177, 176, 77, 199, 91, 158, 175, 88, 154, 254, 117, 191, 234, 217, 255, 58, 90, 135, 150, 95, 98, 182, 56, 212, 76, 139, 213, 223, 220, 123, ], }, PreKeyPublic { pre_key_id: 3, data: vec![ 5, 229, 107, 231, 19, 230, 52, 74, 228, 192, 173, 232, 112, 249, 175, 208, 218, 33, 203, 112, 184, 60, 164, 1, 159, 19, 46, 58, 168, 151, 162, 210, 88, ], }, ], }), }; assert_eq!(bundle, bundle2); } #[test] fn parse_device_list() { let elem: Element = r#" "# .parse() .unwrap(); let list: DeviceList = elem.try_into().unwrap(); let list2 = DeviceList { devices: vec![ Device { id: 1164059891 }, Device { id: 26052318 }, Device { id: 564866972 }, ], }; assert_eq!(list, list2); } #[test] fn parse_encrypted() { let elem: Element = r#"
Mwjp9AESIQVylscLPpj/HlowaTiIsaBj73HCVEllXpVTtMG9EYwRexohBQFd2p/Oq97vAAdLKA09DlcSg0x1xWn260p1jaeyIhAZImIzCiEFhaQ4I+DuQgo6vCLCjHu4uewDZmWHuBl8uJw1IkyZxhUQABgAIjCoEVgVThWlaIlnN3V5Bg1hQX7OD1cvstLD5lH3zZMadL3KeONELESlBbeKmNgcYC/e3HZnbgWzBiic36yNAjAW MwohBTV6dpumL1OxA9MdIFmu2E19+cIWDHWYfhdubvo0hmh6EAAYHCIwNc9/59eeYi8pVZQhMJJMVkKUkFP/yrTfG3o1lfpHGseCqb/JTgtDytQPiYrTpHl2V/mdsM6IPig= MwohBVnhz9pvEj1s1waEHuk5qpQqhUrpavycFz0hq/KYwI8oEAAYCSIwedEGN6MidxyvaPI8zorLcpG0Y7e7ecGkkd5vdDrL7Qt1tXaHb0iDyE/rZZHpFiNN38Izfp5vHv4= SY/SCGPt0CnA2odB
Vas=
"#.parse().unwrap(); let encrypted: Encrypted = elem.try_into().unwrap(); let encrypted2 = Encrypted { header: Header { sid: 564866972, keys: vec![ Key { rid: 1236, prekey: true, data: vec![ 51, 8, 233, 244, 1, 18, 33, 5, 114, 150, 199, 11, 62, 152, 255, 30, 90, 48, 105, 56, 136, 177, 160, 99, 239, 113, 194, 84, 73, 101, 94, 149, 83, 180, 193, 189, 17, 140, 17, 123, 26, 33, 5, 1, 93, 218, 159, 206, 171, 222, 239, 0, 7, 75, 40, 13, 61, 14, 87, 18, 131, 76, 117, 197, 105, 246, 235, 74, 117, 141, 167, 178, 34, 16, 25, 34, 98, 51, 10, 33, 5, 133, 164, 56, 35, 224, 238, 66, 10, 58, 188, 34, 194, 140, 123, 184, 185, 236, 3, 102, 101, 135, 184, 25, 124, 184, 156, 53, 34, 76, 153, 198, 21, 16, 0, 24, 0, 34, 48, 168, 17, 88, 21, 78, 21, 165, 104, 137, 103, 55, 117, 121, 6, 13, 97, 65, 126, 206, 15, 87, 47, 178, 210, 195, 230, 81, 247, 205, 147, 26, 116, 189, 202, 120, 227, 68, 44, 68, 165, 5, 183, 138, 152, 216, 28, 96, 47, 222, 220, 118, 103, 110, 5, 179, 6, 40, 156, 223, 172, 141, 2, 48, 22, ], }, Key { rid: 26052318, prekey: false, data: vec![ 51, 10, 33, 5, 53, 122, 118, 155, 166, 47, 83, 177, 3, 211, 29, 32, 89, 174, 216, 77, 125, 249, 194, 22, 12, 117, 152, 126, 23, 110, 110, 250, 52, 134, 104, 122, 16, 0, 24, 28, 34, 48, 53, 207, 127, 231, 215, 158, 98, 47, 41, 85, 148, 33, 48, 146, 76, 86, 66, 148, 144, 83, 255, 202, 180, 223, 27, 122, 53, 149, 250, 71, 26, 199, 130, 169, 191, 201, 78, 11, 67, 202, 212, 15, 137, 138, 211, 164, 121, 118, 87, 249, 157, 176, 206, 136, 62, 40, ], }, Key { rid: 1164059891, prekey: false, data: vec![ 51, 10, 33, 5, 89, 225, 207, 218, 111, 18, 61, 108, 215, 6, 132, 30, 233, 57, 170, 148, 42, 133, 74, 233, 106, 252, 156, 23, 61, 33, 171, 242, 152, 192, 143, 40, 16, 0, 24, 9, 34, 48, 121, 209, 6, 55, 163, 34, 119, 28, 175, 104, 242, 60, 206, 138, 203, 114, 145, 180, 99, 183, 187, 121, 193, 164, 145, 222, 111, 116, 58, 203, 237, 11, 117, 181, 118, 135, 111, 72, 131, 200, 79, 235, 101, 145, 233, 22, 35, 77, 223, 194, 51, 126, 158, 111, 30, 254, ], }, ], iv: IV { data: vec![73, 143, 210, 8, 99, 237, 208, 41, 192, 218, 135, 65], }, }, payload: Some(Payload { data: vec![85, 171], }), }; assert_eq!(encrypted, encrypted2); } } xmpp-parsers-0.22.0/src/lib.rs000064400000000000000000000171541046102023000142610ustar 00000000000000//! A crate parsing common XMPP elements into Rust structures. //! //! Each module implements the `TryFrom` trait, which takes a //! minidom [`Element`] and returns a `Result` whose value is `Ok` if the //! element parsed correctly, `Err(error::Error)` otherwise. //! //! The returned structure can be manipulated as any Rust structure, with each //! field being public. You can also create the same structure manually, with //! some having `new()` and `with_*()` helper methods to create them. //! //! Once you are happy with your structure, you can serialise it back to an //! [`Element`], using either `From` or `Into`, which give you what //! you want to be sending on the wire. //! //! [`Element`]: ../minidom/element/struct.Element.html // Copyright (c) 2017-2019 Emmanuel Gil Peyrot // Copyright (c) 2017-2019 Maxime “pep” Buquet // // 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/. #![warn(missing_docs)] #![deny( non_camel_case_types, non_snake_case, unsafe_code, unused_variables, unused_mut, dead_code )] #![cfg_attr(docsrs, feature(doc_cfg))] #![cfg_attr(docsrs, doc(auto_cfg))] extern crate alloc; pub use blake2; pub use jid; pub use minidom; pub use sha1; pub use sha2; pub use sha3; // We normally only reexport entire crates, but xso is a special case since it uses proc macros // which require it to be directly imported as a crate. The only useful symbol we have to reexport // are its error types, which we expose in our return types. pub use xso::error::{Error, FromElementError}; /// XML namespace definitions used through XMPP. pub mod ns; #[macro_use] mod util; /// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core pub mod bind; /// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core pub mod iq; /// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core pub mod message; /// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core pub mod presence; /// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core pub mod sasl; /// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core pub mod stanza_error; /// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core pub mod starttls; /// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core pub mod stream; /// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core pub mod stream_error; /// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core pub mod stream_features; /// RFC 6121: Extensible Messaging and Presence Protocol (XMPP): Instant Messaging and Presence pub mod roster; /// RFC 7395: An Extensible Messaging and Presence Protocol (XMPP) Subprotocol for WebSocket pub mod websocket; /// XEP-0004: Data Forms pub mod data_forms; /// XEP-0030: Service Discovery pub mod disco; /// XEP-0045: Multi-User Chat pub mod muc; /// XEP-0047: In-Band Bytestreams pub mod ibb; /// XEP-0048: Bookmarks pub mod bookmarks; /// XEP-0049: Private XML storage pub mod private; /// XEP-0054: vcard-temp pub mod vcard; /// XEP-0059: Result Set Management pub mod rsm; /// XEP-0060: Publish-Subscribe pub mod pubsub; /// XEP-0066: OOB pub mod oob; /// XEP-0070: Verifying HTTP Requests via XMPP pub mod confirm; /// XEP-0071: XHTML-IM pub mod xhtml; /// XEP-0077: In-Band Registration pub mod ibr; /// XEP-0082: XMPP Date and Time Profiles pub mod date; /// XEP-0084: User Avatar pub mod avatar; /// XEP-0085: Chat State Notifications pub mod chatstates; /// XEP-0092: Software Version pub mod version; /// XEP-0107: User Mood pub mod mood; /// XEP-0114: Jabber Component Protocol pub mod component; /// XEP-0115: Entity Capabilities pub mod caps; /// XEP-0118: User Tune pub mod tune; /// XEP-0122: Data Forms Validation pub mod data_forms_validate; ///XEP-0153: vCard-Based Avatars pub mod vcard_update; /// XEP-0157: Contact Addresses for XMPP Services pub mod server_info; /// XEP-0166: Jingle pub mod jingle; /// XEP-0167: Jingle RTP Sessions pub mod jingle_rtp; /// XEP-0172: User Nickname pub mod nick; /// XEP-0176: Jingle ICE-UDP Transport Method pub mod jingle_ice_udp; /// XEP-0177: Jingle Raw UDP Transport Method pub mod jingle_raw_udp; /// XEP-0184: Message Delivery Receipts pub mod receipts; /// XEP-0191: Blocking Command pub mod blocking; /// XEP-0198: Stream Management pub mod sm; /// XEP-0199: XMPP Ping pub mod ping; /// XEP-0202: Entity Time pub mod time; /// XEP-0203: Delayed Delivery pub mod delay; /// XEP-0215: External Service Discovery pub mod extdisco; /// XEP-0221: Data Forms Media Element pub mod media_element; /// XEP-0224: Attention pub mod attention; /// XEP-0231: Bits of Binary pub mod bob; /// XEP-0234: Jingle File Transfer pub mod jingle_ft; /// XEP-0257: Client Certificate Management for SASL EXTERNAL pub mod cert_management; /// XEP-0260: Jingle SOCKS5 Bytestreams Transport Method pub mod jingle_s5b; /// XEP-0261: Jingle In-Band Bytestreams Transport Method pub mod jingle_ibb; /// XEP-0264: Jingle Content Thumbnails pub mod jingle_thumbnails; /// XEP-0280: Message Carbons pub mod carbons; /// XEP-0293: Jingle RTP Feedback Negotiation pub mod jingle_rtcp_fb; /// XEP-0294: Jingle RTP Header Extensions Negotiation pub mod jingle_rtp_hdrext; /// XEP-0297: Stanza Forwarding pub mod forwarding; /// XEP-0300: Use of Cryptographic Hash Functions in XMPP pub mod hashes; /// XEP-0301: In-Band Real Time Text pub mod rtt; /// XEP-0308: Last Message Correction pub mod message_correct; /// XEP-0313: Message Archive Management pub mod mam; /// XEP-0319: Last User Interaction in Presence pub mod idle; /// XEP-0320: Use of DTLS-SRTP in Jingle Sessions pub mod jingle_dtls_srtp; /// XEP-0328: JID Prep pub mod jid_prep; /// XEP-0335: JSON Containers pub mod json_containers; /// XEP-0338: Jingle Grouping Framework pub mod jingle_grouping; /// XEP-0339: Source-Specific Media Attributes in Jingle pub mod jingle_ssma; /// XEP-0352: Client State Indication pub mod csi; /// XEP-0353: Jingle Message Initiation pub mod jingle_message; /// XEP-0357: Push Notifications pub mod push; /// XEP-0359: Unique and Stable Stanza IDs pub mod stanza_id; /// XEP-0363: HTTP File Upload pub mod http_upload; /// XEP-0373: OpenPGP for XMPP pub mod openpgp; /// XEP-0377: Spam Reporting pub mod spam_reporting; /// XEP-0380: Explicit Message Encryption pub mod eme; /// XEP-0380: OMEMO Encryption (experimental version 0.3.0) pub mod legacy_omemo; /// XEP-0386: Bind 2 pub mod bind2; /// XEP-0388: Extensible SASL Profile pub mod sasl2; /// XEP-0390: Entity Capabilities 2.0 pub mod ecaps2; /// XEP-0402: PEP Native Bookmarks pub mod bookmarks2; /// XEP-0421: Anonymous unique occupant identifiers for MUCs pub mod occupant_id; /// XEP-0441: Message Archive Management Preferences pub mod mam_prefs; /// XEP-0440: SASL Channel-Binding Type Capability pub mod sasl_cb; /// XEP-0444: Message Reactions pub mod reactions; /// XEP-0478: Stream Limits Advertisement pub mod stream_limits; /// XEP-0484: Fast Authentication Streamlining Tokens pub mod fast; /// XEP-0490: Message Displayed Synchronization pub mod message_displayed; #[cfg(test)] mod tests { #[test] fn reexports() { #[allow(unused_imports)] use crate::blake2; #[allow(unused_imports)] use crate::jid; #[allow(unused_imports)] use crate::minidom; #[allow(unused_imports)] use crate::sha1; #[allow(unused_imports)] use crate::sha2; #[allow(unused_imports)] use crate::sha3; } } xmpp-parsers-0.22.0/src/mam.rs000064400000000000000000000301221046102023000142530ustar 00000000000000// Copyright (c) 2017-2021 Emmanuel Gil Peyrot // // 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 xso::{AsXml, FromXml}; use crate::data_forms::DataForm; use crate::date::DateTime; use crate::forwarding::Forwarded; use crate::iq::{IqGetPayload, IqResultPayload, IqSetPayload}; use crate::message::MessagePayload; use crate::ns; use crate::pubsub::NodeName; use crate::rsm::{SetQuery, SetResult}; generate_id!( /// An identifier matching a result message to the query requesting it. QueryId ); /// Starts a query to the archive. #[derive(FromXml, AsXml, Debug)] #[xml(namespace = ns::MAM, name = "query")] pub struct Query { /// An optional identifier for matching forwarded messages to this /// query. #[xml(attribute(default))] pub queryid: Option, /// Must be set to Some when querying a PubSub node’s archive. #[xml(attribute(default))] pub node: Option, /// Used for filtering the results. #[xml(child(default))] pub form: Option, /// Used for paging through results. #[xml(child(default))] pub set: Option, /// Used for reversing the order of the results. #[xml(flag(name = "flip-page"))] pub flip_page: bool, } impl IqGetPayload for Query {} impl IqSetPayload for Query {} impl IqResultPayload for Query {} /// The wrapper around forwarded stanzas. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::MAM, name = "result")] pub struct Result_ { /// The stanza-id under which the archive stored this stanza. #[xml(attribute)] pub id: String, /// The same queryid as the one requested in the /// [query](struct.Query.html). #[xml(attribute(default))] pub queryid: Option, /// The actual stanza being forwarded. #[xml(child)] pub forwarded: Forwarded, } impl MessagePayload for Result_ {} /// Notes the end of a page in a query. #[derive(FromXml, AsXml, Debug, Clone, PartialEq)] #[xml(namespace = ns::MAM, name = "fin")] pub struct Fin { /// True when the end of a MAM query has been reached. #[xml(attribute(default))] pub complete: bool, /// Describes the current page, it should contain at least [first] /// (with an [index]) and [last], and generally [count]. /// /// [first]: ../rsm/struct.SetResult.html#structfield.first /// [index]: ../rsm/struct.SetResult.html#structfield.first_index /// [last]: ../rsm/struct.SetResult.html#structfield.last /// [count]: ../rsm/struct.SetResult.html#structfield.count #[xml(child)] pub set: SetResult, } impl IqResultPayload for Fin {} /// Metadata of the first message in the archive. #[derive(FromXml, AsXml, Debug, Clone, PartialEq)] #[xml(namespace = ns::MAM, name = "start")] pub struct Start { /// The id of the first message in the archive. #[xml(attribute)] pub id: String, /// Time at which that message was sent. #[xml(attribute)] pub timestamp: DateTime, } /// Metadata of the last message in the archive. #[derive(FromXml, AsXml, Debug, Clone, PartialEq)] #[xml(namespace = ns::MAM, name = "end")] pub struct End { /// The id of the last message in the archive. #[xml(attribute)] pub id: String, /// Time at which that message was sent. #[xml(attribute)] pub timestamp: DateTime, } /// Request an archive for its metadata. #[derive(FromXml, AsXml, Debug, Clone, PartialEq)] #[xml(namespace = ns::MAM, name = "metadata")] pub struct MetadataQuery; impl IqGetPayload for MetadataQuery {} /// Response from the archive, containing the start and end metadata if it isn’t empty. #[derive(FromXml, AsXml, Debug, Clone, PartialEq)] #[xml(namespace = ns::MAM, name = "metadata")] pub struct MetadataResponse { /// Metadata about the first message in the archive. #[xml(child(default))] pub start: Option, /// Metadata about the last message in the archive. #[xml(child(default))] pub end: Option, } impl IqResultPayload for MetadataResponse {} #[cfg(test)] mod tests { use super::*; use minidom::Element; use xso::error::{Error, FromElementError}; #[cfg(target_pointer_width = "32")] #[test] fn test_size() { assert_size!(QueryId, 12); assert_size!(Query, 108); assert_size!(Result_, 176); assert_size!(Fin, 44); assert_size!(Start, 28); assert_size!(End, 28); assert_size!(MetadataQuery, 0); assert_size!(MetadataResponse, 56); } #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(QueryId, 24); assert_size!(Query, 216); assert_size!(Result_, 336); assert_size!(Fin, 88); assert_size!(Start, 40); assert_size!(End, 40); assert_size!(MetadataQuery, 0); assert_size!(MetadataResponse, 80); } #[test] fn test_query() { let elem: Element = "".parse().unwrap(); Query::try_from(elem).unwrap(); } #[test] fn test_result() { #[cfg(not(feature = "component"))] let elem: Element = r#" Hail to thee "# .parse() .unwrap(); #[cfg(feature = "component")] let elem: Element = r#" Hail to thee "#.parse().unwrap(); Result_::try_from(elem).unwrap(); } #[test] fn test_fin() { let elem: Element = r#" 28482-98726-73623 09af3-cc343-b409f "# .parse() .unwrap(); Fin::try_from(elem).unwrap(); } #[test] fn test_query_x() { let elem: Element = r#" urn:xmpp:mam:2 juliet@capulet.lit "# .parse() .unwrap(); Query::try_from(elem).unwrap(); } #[test] fn test_query_x_set() { let elem: Element = r#" urn:xmpp:mam:2 2010-08-07T00:00:00Z 10 "# .parse() .unwrap(); Query::try_from(elem).unwrap(); } #[test] fn test_query_x_set_flipped() { let elem: Element = r#" urn:xmpp:mam:2 2010-08-07T00:00:00Z 10 "# .parse() .unwrap(); Query::try_from(elem).unwrap(); } #[test] fn test_metadata() { let elem: Element = r"".parse().unwrap(); MetadataQuery::try_from(elem).unwrap(); let elem: Element = r" " .parse() .unwrap(); let metadata = MetadataResponse::try_from(elem).unwrap(); let start = metadata.start.unwrap(); let end = metadata.end.unwrap(); assert_eq!(start.id, "YWxwaGEg"); assert_eq!(start.timestamp.0.timestamp(), 1219439344); assert_eq!(end.id, "b21lZ2Eg"); assert_eq!(end.timestamp.0.timestamp(), 1587393261); } #[test] fn test_invalid_child() { let elem: Element = "" .parse() .unwrap(); let error = Query::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!(message, "Unknown child in Query element."); } #[test] fn test_serialise_empty() { let elem: Element = "".parse().unwrap(); let replace = Query { queryid: None, node: None, form: None, set: None, flip_page: false, }; let elem2 = replace.into(); assert_eq!(elem, elem2); } #[test] fn test_serialize_query_with_form() { let reference: Element = "urn:xmpp:mam:2juliet@capulet.lit" .parse() .unwrap(); let elem: Element = "urn:xmpp:mam:2juliet@capulet.lit" .parse() .unwrap(); let form = DataForm::try_from(elem).unwrap(); let query = Query { queryid: None, node: None, set: None, form: Some(form), flip_page: true, }; let serialized: Element = query.into(); assert_eq!(serialized, reference); } #[test] fn test_serialize_result() { let reference: Element = "" .parse() .unwrap(); let elem: Element = "" .parse() .unwrap(); let forwarded = Forwarded::try_from(elem).unwrap(); let result = Result_ { id: String::from("28482-98726-73623"), queryid: Some(QueryId(String::from("f27"))), forwarded, }; let serialized: Element = result.into(); assert_eq!(serialized, reference); } #[test] fn test_serialize_fin() { let reference: Element = "28482-98726-7362309af3-cc343-b409f" .parse() .unwrap(); let elem: Element = "28482-98726-7362309af3-cc343-b409f" .parse() .unwrap(); let set = SetResult::try_from(elem).unwrap(); let fin = Fin { set, complete: false, }; let serialized: Element = fin.into(); assert_eq!(serialized, reference); } } xmpp-parsers-0.22.0/src/mam_prefs.rs000064400000000000000000000067231046102023000154640ustar 00000000000000// Copyright (c) 2021 Emmanuel Gil Peyrot // // 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 xso::{AsXml, FromXml}; use crate::iq::{IqGetPayload, IqResultPayload, IqSetPayload}; use crate::ns; use jid::Jid; generate_attribute!( /// Notes the default archiving preference for the user. DefaultPrefs, "default", { /// The default is to always log messages in the archive. Always => "always", /// The default is to never log messages in the archive. Never => "never", /// The default is to log messages in the archive only for contacts /// present in the user’s [roster](../roster/index.html). Roster => "roster", } ); /// Controls the archiving preferences of the user. #[derive(FromXml, AsXml, Debug, Clone, PartialEq)] #[xml(namespace = ns::MAM, name = "prefs")] pub struct Prefs { /// The default preference for JIDs in neither /// [always](#structfield.always) or [never](#structfield.never) lists. #[xml(attribute = "default")] pub default_: DefaultPrefs, /// The set of JIDs for which to always store messages in the archive. #[xml(extract(default, fields(extract(n = .., name = "jid", fields(text(type_ = Jid))))))] pub always: Vec, /// The set of JIDs for which to never store messages in the archive. #[xml(extract(default, fields(extract(n = .., name = "jid", fields(text(type_ = Jid))))))] pub never: Vec, } impl IqGetPayload for Prefs {} impl IqSetPayload for Prefs {} impl IqResultPayload for Prefs {} #[cfg(test)] mod tests { use super::*; use jid::BareJid; use minidom::Element; #[cfg(target_pointer_width = "32")] #[test] fn test_size() { assert_size!(DefaultPrefs, 1); assert_size!(Prefs, 28); } #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(DefaultPrefs, 1); assert_size!(Prefs, 56); } #[test] fn test_prefs_get() { let elem: Element = "" .parse() .unwrap(); let prefs = Prefs::try_from(elem).unwrap(); assert!(prefs.always.is_empty()); assert!(prefs.never.is_empty()); let elem: Element = r#" "# .parse() .unwrap(); let prefs = Prefs::try_from(elem).unwrap(); assert!(prefs.always.is_empty()); assert!(prefs.never.is_empty()); } #[test] fn test_prefs_result() { let elem: Element = r#" romeo@montague.lit montague@montague.lit "# .parse() .unwrap(); let prefs = Prefs::try_from(elem).unwrap(); assert_eq!(prefs.always, [BareJid::new("romeo@montague.lit").unwrap()]); assert_eq!( prefs.never, [BareJid::new("montague@montague.lit").unwrap()] ); let elem2 = Element::from(prefs.clone()); println!("{:?}", elem2); let prefs2 = Prefs::try_from(elem2).unwrap(); assert_eq!(prefs.default_, prefs2.default_); assert_eq!(prefs.always, prefs2.always); assert_eq!(prefs.never, prefs2.never); } } xmpp-parsers-0.22.0/src/media_element.rs000064400000000000000000000215341046102023000163000ustar 00000000000000// Copyright (c) 2017 Emmanuel Gil Peyrot // // 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 xso::{text::EmptyAsError, AsXml, FromXml}; use crate::ns; /// Represents an URI used in a media element. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::MEDIA_ELEMENT, name = "uri")] pub struct Uri { /// The MIME type of the URI referenced. /// /// See the [IANA MIME Media Types Registry][1] for a list of /// registered types, but unregistered or yet-to-be-registered are /// accepted too. /// /// [1]: #[xml(attribute(name = "type"))] pub type_: String, /// The actual URI contained. #[xml(text(codec = EmptyAsError))] pub uri: String, } /// References a media element, to be used in [data /// forms](../data_forms/index.html). #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::MEDIA_ELEMENT, name = "media")] pub struct MediaElement { /// The recommended display width in pixels. #[xml(attribute(default))] pub width: Option, /// The recommended display height in pixels. #[xml(attribute(default))] pub height: Option, /// A list of URIs referencing this media. #[xml(child(n = ..))] pub uris: Vec, } #[cfg(test)] mod tests { use super::*; use crate::data_forms::DataForm; use minidom::Element; use xso::error::{Error, FromElementError}; #[cfg(target_pointer_width = "32")] #[test] fn test_size() { assert_size!(Uri, 24); assert_size!(MediaElement, 28); } #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(Uri, 48); assert_size!(MediaElement, 56); } #[test] fn test_simple() { let elem: Element = "".parse().unwrap(); let media = MediaElement::try_from(elem).unwrap(); assert!(media.width.is_none()); assert!(media.height.is_none()); assert!(media.uris.is_empty()); } #[test] fn test_width_height() { let elem: Element = "" .parse() .unwrap(); let media = MediaElement::try_from(elem).unwrap(); assert_eq!(media.width.unwrap(), 32); assert_eq!(media.height.unwrap(), 32); } #[test] fn test_uri() { let elem: Element = "https://example.org/".parse().unwrap(); let media = MediaElement::try_from(elem).unwrap(); assert_eq!(media.uris.len(), 1); assert_eq!(media.uris[0].type_, "text/html"); assert_eq!(media.uris[0].uri, "https://example.org/"); } #[test] fn test_invalid_width_height() { let elem: Element = "" .parse() .unwrap(); let error = MediaElement::try_from(elem).unwrap_err(); let error = match error { FromElementError::Invalid(Error::TextParseError(error)) if error.is::() => { error } _ => panic!(), }; assert_eq!(error.to_string(), "cannot parse integer from empty string"); let elem: Element = "" .parse() .unwrap(); let error = MediaElement::try_from(elem).unwrap_err(); let error = match error { FromElementError::Invalid(Error::TextParseError(error)) if error.is::() => { error } _ => panic!(), }; assert_eq!(error.to_string(), "invalid digit found in string"); let elem: Element = "" .parse() .unwrap(); let error = MediaElement::try_from(elem).unwrap_err(); let error = match error { FromElementError::Invalid(Error::TextParseError(error)) if error.is::() => { error } _ => panic!(), }; assert_eq!(error.to_string(), "cannot parse integer from empty string"); let elem: Element = "" .parse() .unwrap(); let error = MediaElement::try_from(elem).unwrap_err(); let error = match error { FromElementError::Invalid(Error::TextParseError(error)) if error.is::() => { error } _ => panic!(), }; assert_eq!(error.to_string(), "invalid digit found in string"); } #[test] #[cfg_attr(feature = "disable-validation", should_panic = "Result::unwrap_err")] fn test_unknown_child() { let elem: Element = "" .parse() .unwrap(); let error = MediaElement::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!(message, "Unknown child in MediaElement element."); } #[test] fn test_bad_uri() { let elem: Element = "https://example.org/" .parse() .unwrap(); let error = MediaElement::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!( message, "Required attribute field 'type_' on Uri element missing." ); let elem: Element = "" .parse() .unwrap(); let error = MediaElement::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!(message, "Empty text node."); } #[test] fn test_xep_ex1() { let elem: Element = r#" http://victim.example.com/challenges/speech.wav?F3A6292C cid:sha1+a15a505e360702b79c75a5f67773072ed392f52a@bob.xmpp.org http://victim.example.com/challenges/speech.mp3?F3A6292C "# .parse() .unwrap(); let media = MediaElement::try_from(elem).unwrap(); assert!(media.width.is_none()); assert!(media.height.is_none()); assert_eq!(media.uris.len(), 3); assert_eq!(media.uris[0].type_, "audio/x-wav"); assert_eq!( media.uris[0].uri, "http://victim.example.com/challenges/speech.wav?F3A6292C" ); assert_eq!(media.uris[1].type_, "audio/ogg; codecs=speex"); assert_eq!( media.uris[1].uri, "cid:sha1+a15a505e360702b79c75a5f67773072ed392f52a@bob.xmpp.org" ); assert_eq!(media.uris[2].type_, "audio/mpeg"); assert_eq!( media.uris[2].uri, "http://victim.example.com/challenges/speech.mp3?F3A6292C" ); } #[test] fn test_xep_ex2() { let elem: Element = r#" http://www.victim.com/challenges/ocr.jpeg?F3A6292C cid:sha1+f24030b8d91d233bac14777be5ab531ca3b9f102@bob.xmpp.org "# .parse() .unwrap(); let form = DataForm::try_from(elem).unwrap(); assert_eq!(form.fields.len(), 1); assert_eq!(form.fields[0].var.as_deref(), Some("ocr")); assert_eq!(form.fields[0].media[0].width, Some(290)); assert_eq!(form.fields[0].media[0].height, Some(80)); assert_eq!(form.fields[0].media[0].uris[0].type_, "image/jpeg"); assert_eq!( form.fields[0].media[0].uris[0].uri, "http://www.victim.com/challenges/ocr.jpeg?F3A6292C" ); assert_eq!(form.fields[0].media[0].uris[1].type_, "image/jpeg"); assert_eq!( form.fields[0].media[0].uris[1].uri, "cid:sha1+f24030b8d91d233bac14777be5ab531ca3b9f102@bob.xmpp.org" ); } } xmpp-parsers-0.22.0/src/message.rs000064400000000000000000000455371046102023000151450ustar 00000000000000// Copyright (c) 2017 Emmanuel Gil Peyrot // // 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 core::fmt; use core::ops::{Deref, DerefMut}; use std::borrow::{Borrow, Cow}; use crate::ns; use alloc::collections::BTreeMap; use jid::Jid; use minidom::Element; use xso::{ error::{Error, FromElementError}, AsOptionalXmlText, AsXml, FromXml, FromXmlText, }; /// Should be implemented on every known payload of a ``. pub trait MessagePayload: TryFrom + Into {} generate_attribute!( /// The type of a message. MessageType, "type", { /// Standard instant messaging message. Chat => "chat", /// Notifies that an error happened. Error => "error", /// Standard group instant messaging message. Groupchat => "groupchat", /// Used by servers to notify users when things happen. Headline => "headline", /// This is an email-like message, it usually contains a /// [subject](struct.Subject.html). Normal => "normal", }, Default = Normal ); generate_id!( /// Id field in a [`Message`], if any. /// /// This field is not mandatory on incoming messages, but may be useful for moderation/correction, /// especially for outgoing messages. Id ); /// Wrapper type to represent an optional `xml:lang` attribute. /// /// This is necessary because we do not want to emit empty `xml:lang` /// attributes. #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Default)] pub struct Lang(pub String); impl Deref for Lang { type Target = String; fn deref(&self) -> &Self::Target { &self.0 } } impl DerefMut for Lang { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } impl fmt::Display for Lang { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::Display::fmt(&self.0, f) } } impl FromXmlText for Lang { fn from_xml_text(s: String) -> Result { Ok(Self(s)) } } impl AsOptionalXmlText for Lang { fn as_optional_xml_text(&self) -> Result>, Error> { if self.0.is_empty() { Ok(None) } else { Ok(Some(Cow::Borrowed(&self.0))) } } } impl Borrow for Lang { fn borrow(&self) -> &str { &self.0 } } impl From for Lang { fn from(other: String) -> Self { Self(other) } } impl From<&str> for Lang { fn from(other: &str) -> Self { Self(other.to_owned()) } } impl PartialEq for Lang { fn eq(&self, rhs: &str) -> bool { self.0 == rhs } } impl PartialEq<&str> for Lang { fn eq(&self, rhs: &&str) -> bool { self.0 == *rhs } } impl Lang { /// Create a new, empty `Lang`. pub fn new() -> Self { Self(String::new()) } } /// Threading meta-information. #[derive(FromXml, AsXml, Debug, Clone, PartialEq)] #[xml(namespace = ns::DEFAULT_NS, name = "thread")] pub struct Thread { /// The parent of the thread, when creating a subthread. #[xml(attribute(default))] pub parent: Option, /// A thread identifier, so that other people can specify to which message /// they are replying. #[xml(text)] pub id: String, } /// The main structure representing the `` stanza. #[derive(FromXml, AsXml, Debug, Clone, PartialEq)] #[xml(namespace = ns::DEFAULT_NS, name = "message")] pub struct Message { /// The JID emitting this stanza. #[xml(attribute(default))] pub from: Option, /// The recipient of this stanza. #[xml(attribute(default))] pub to: Option, /// The @id attribute of this stanza, which is required in order to match a /// request with its response. #[xml(attribute(default))] pub id: Option, /// The type of this message. #[xml(attribute(name = "type", default))] pub type_: MessageType, /// A list of bodies, sorted per language. Use /// [get_best_body()](#method.get_best_body) to access them on reception. #[xml(extract(n = .., name = "body", fields( lang(type_ = Lang, default), text(type_ = String), )))] pub bodies: BTreeMap, /// A list of subjects, sorted per language. Use /// [get_best_subject()](#method.get_best_subject) to access them on /// reception. #[xml(extract(n = .., name = "subject", fields( lang(type_ = Lang, default), text(type_ = String), )))] pub subjects: BTreeMap, /// An optional thread identifier, so that other people can reply directly /// to this message. #[xml(child(default))] pub thread: Option, /// A list of the extension payloads contained in this stanza. #[xml(element(n = ..))] pub payloads: Vec, } impl Message { /// Creates a new `` stanza of type Chat for the given recipient. /// This is equivalent to the [`Message::chat`] method. pub fn new>>(to: J) -> Message { Message { from: None, to: to.into(), id: None, type_: MessageType::Chat, bodies: BTreeMap::new(), subjects: BTreeMap::new(), thread: None, payloads: vec![], } } /// Creates a new `` stanza of a certain type for the given recipient. pub fn new_with_type>>(type_: MessageType, to: J) -> Message { Message { from: None, to: to.into(), id: None, type_, bodies: BTreeMap::new(), subjects: BTreeMap::new(), thread: None, payloads: vec![], } } /// Creates a Message of type Chat pub fn chat>>(to: J) -> Message { Self::new_with_type(MessageType::Chat, to) } /// Creates a Message of type Error pub fn error>>(to: J) -> Message { Self::new_with_type(MessageType::Error, to) } /// Creates a Message of type Groupchat pub fn groupchat>>(to: J) -> Message { Self::new_with_type(MessageType::Groupchat, to) } /// Creates a Message of type Headline pub fn headline>>(to: J) -> Message { Self::new_with_type(MessageType::Headline, to) } /// Creates a Message of type Normal pub fn normal>>(to: J) -> Message { Self::new_with_type(MessageType::Normal, to) } /// Appends a body in given lang to the Message pub fn with_body(mut self, lang: Lang, body: String) -> Message { self.bodies.insert(lang, body); self } /// Set a payload inside this message. pub fn with_payload(mut self, payload: P) -> Message { self.payloads.push(payload.into()); self } /// Set the payloads of this message. pub fn with_payloads(mut self, payloads: Vec) -> Message { self.payloads = payloads; self } fn get_best<'a, T>( map: &'a BTreeMap, preferred_langs: Vec<&str>, ) -> Option<(Lang, &'a T)> { if map.is_empty() { return None; } for lang in preferred_langs { if let Some(value) = map.get(lang) { return Some((Lang::from(lang), value)); } } if let Some(value) = map.get("") { return Some((Lang::new(), value)); } map.iter().map(|(lang, value)| (lang.clone(), value)).next() } fn get_best_cloned>( map: &BTreeMap, preferred_langs: Vec<&str>, ) -> Option<(Lang, T)> { if let Some((lang, item)) = Self::get_best::(map, preferred_langs) { Some((lang, item.to_owned())) } else { None } } /// Returns the best matching body from a list of languages. /// /// For instance, if a message contains both an xml:lang='de', an xml:lang='fr' and an English /// body without an xml:lang attribute, and you pass ["fr", "en"] as your preferred languages, /// `Some(("fr", the_second_body))` will be returned. /// /// If no body matches, an undefined body will be returned. pub fn get_best_body(&self, preferred_langs: Vec<&str>) -> Option<(Lang, &String)> { Message::get_best::(&self.bodies, preferred_langs) } /// Cloned variant of [`Message::get_best_body`] pub fn get_best_body_cloned(&self, preferred_langs: Vec<&str>) -> Option<(Lang, String)> { Message::get_best_cloned::(&self.bodies, preferred_langs) } /// Returns the best matching subject from a list of languages. /// /// For instance, if a message contains both an xml:lang='de', an xml:lang='fr' and an English /// subject without an xml:lang attribute, and you pass ["fr", "en"] as your preferred /// languages, `Some(("fr", the_second_subject))` will be returned. /// /// If no subject matches, an undefined subject will be returned. pub fn get_best_subject(&self, preferred_langs: Vec<&str>) -> Option<(Lang, &String)> { Message::get_best::(&self.subjects, preferred_langs) } /// Cloned variant of [`Message::get_best_subject`] pub fn get_best_subject_cloned(&self, preferred_langs: Vec<&str>) -> Option<(Lang, String)> { Message::get_best_cloned::(&self.subjects, preferred_langs) } /// Try to extract the given payload type from the message's payloads. /// /// Returns the first matching payload element as parsed struct or its /// parse error. If no element matches, `Ok(None)` is returned. If an /// element matches, but fails to parse, it is nonetheless removed from /// the message. /// /// Elements which do not match the given type are not removed. pub fn extract_payload>( &mut self, ) -> Result, Error> { let mut buf = Vec::with_capacity(self.payloads.len()); let mut iter = self.payloads.drain(..); let mut result = Ok(None); for item in &mut iter { match T::try_from(item) { Ok(v) => { result = Ok(Some(v)); break; } Err(FromElementError::Mismatch(residual)) => { buf.push(residual); } Err(FromElementError::Invalid(other)) => { result = Err(other); break; } } } buf.extend(iter); core::mem::swap(&mut buf, &mut self.payloads); result } /// Tries to extract the payload, warning when parsing fails. /// /// This method uses [`Message::extract_payload`], but removes the error /// case by simply warning to the current logger. #[cfg(feature = "log")] pub fn extract_valid_payload>( &mut self, ) -> Option { match self.extract_payload::() { Ok(opt) => opt, Err(e) => { // TODO: xso should support human-readable name for T log::warn!("Failed to parse payload: {e}"); None } } } } #[cfg(test)] mod tests { use super::*; #[cfg(target_pointer_width = "32")] #[test] fn test_size() { assert_size!(MessageType, 1); assert_size!(Thread, 24); assert_size!(Message, 108); } #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(MessageType, 1); assert_size!(Thread, 48); assert_size!(Message, 216); } #[test] fn test_simple() { #[cfg(not(feature = "component"))] let elem: Element = "".parse().unwrap(); #[cfg(feature = "component")] let elem: Element = "" .parse() .unwrap(); let message = Message::try_from(elem).unwrap(); assert_eq!(message.from, None); assert_eq!(message.to, None); assert_eq!(message.id, None); assert_eq!(message.type_, MessageType::Normal); assert!(message.payloads.is_empty()); } #[test] fn test_serialise() { #[cfg(not(feature = "component"))] let elem: Element = "".parse().unwrap(); #[cfg(feature = "component")] let elem: Element = "" .parse() .unwrap(); let mut message = Message::new(None); message.type_ = MessageType::Normal; let elem2 = message.into(); assert_eq!(elem, elem2); } #[test] fn test_body() { #[cfg(not(feature = "component"))] let elem: Element = "Hello world!".parse().unwrap(); #[cfg(feature = "component")] let elem: Element = "Hello world!".parse().unwrap(); let elem1 = elem.clone(); let message = Message::try_from(elem).unwrap(); assert_eq!(message.bodies[""], "Hello world!"); { let (lang, body) = message.get_best_body(vec!["en"]).unwrap(); assert_eq!(lang, ""); assert_eq!(body, &"Hello world!"); } let elem2 = message.into(); assert_eq!(elem1, elem2); } #[test] fn test_serialise_body() { #[cfg(not(feature = "component"))] let elem: Element = "Hello world!".parse().unwrap(); #[cfg(feature = "component")] let elem: Element = "Hello world!".parse().unwrap(); let mut message = Message::new(Jid::new("coucou@example.org").unwrap()); message .bodies .insert(Lang::from(""), "Hello world!".to_owned()); let elem2 = message.into(); assert_eq!(elem, elem2); } #[test] fn test_subject() { #[cfg(not(feature = "component"))] let elem: Element = "Hello world!".parse().unwrap(); #[cfg(feature = "component")] let elem: Element = "Hello world!".parse().unwrap(); let elem1 = elem.clone(); let message = Message::try_from(elem).unwrap(); assert_eq!(message.subjects[""], "Hello world!",); { let (lang, subject) = message.get_best_subject(vec!["en"]).unwrap(); assert_eq!(lang, ""); assert_eq!(subject, "Hello world!"); } // Test cloned variant. { let (lang, subject) = message.get_best_subject_cloned(vec!["en"]).unwrap(); assert_eq!(lang, ""); assert_eq!(subject, "Hello world!"); } let elem2 = message.into(); assert_eq!(elem1, elem2); } #[test] fn get_best_body() { #[cfg(not(feature = "component"))] let elem: Element = "Hallo Welt!Salut le monde !Hello world!".parse().unwrap(); #[cfg(feature = "component")] let elem: Element = "Hallo Welt!Salut le monde !Hello world!".parse().unwrap(); let message = Message::try_from(elem).unwrap(); // Tests basic feature. { let (lang, body) = message.get_best_body(vec!["fr"]).unwrap(); assert_eq!(lang, "fr"); assert_eq!(body, "Salut le monde !"); } // Tests order. { let (lang, body) = message.get_best_body(vec!["en", "de"]).unwrap(); assert_eq!(lang, "de"); assert_eq!(body, "Hallo Welt!"); } // Tests fallback. { let (lang, body) = message.get_best_body(vec![]).unwrap(); assert_eq!(lang, ""); assert_eq!(body, "Hello world!"); } // Tests fallback. { let (lang, body) = message.get_best_body(vec!["ja"]).unwrap(); assert_eq!(lang, ""); assert_eq!(body, "Hello world!"); } // Test cloned variant. { let (lang, body) = message.get_best_body_cloned(vec!["ja"]).unwrap(); assert_eq!(lang, ""); assert_eq!(body, "Hello world!"); } let message = Message::new(None); // Tests without a body. assert_eq!(message.get_best_body(vec!("ja")), None); } #[test] fn test_attention() { #[cfg(not(feature = "component"))] let elem: Element = "".parse().unwrap(); #[cfg(feature = "component")] let elem: Element = "".parse().unwrap(); let elem1 = elem.clone(); let message = Message::try_from(elem).unwrap(); let elem2 = message.into(); assert_eq!(elem1, elem2); } #[test] fn test_extract_payload() { use super::super::attention::Attention; use super::super::pubsub; #[cfg(not(feature = "component"))] let elem: Element = "".parse().unwrap(); #[cfg(feature = "component")] let elem: Element = "".parse().unwrap(); let mut message = Message::try_from(elem).unwrap(); assert_eq!(message.payloads.len(), 1); match message.extract_payload::() { Ok(None) => (), other => panic!("unexpected result: {:?}", other), }; assert_eq!(message.payloads.len(), 1); match message.extract_payload::() { Ok(Some(_)) => (), other => panic!("unexpected result: {:?}", other), }; assert_eq!(message.payloads.len(), 0); } } xmpp-parsers-0.22.0/src/message_correct.rs000064400000000000000000000062241046102023000166540ustar 00000000000000// Copyright (c) 2017 Emmanuel Gil Peyrot // // 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 xso::{AsXml, FromXml}; use crate::message::{Id, MessagePayload}; use crate::ns; /// Defines that the message containing this payload should replace a /// previous message, identified by the id. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::MESSAGE_CORRECT, name = "replace")] pub struct Replace { /// The 'id' attribute of the message getting corrected. #[xml(attribute)] pub id: Id, } impl MessagePayload for Replace {} #[cfg(test)] mod tests { use super::*; use minidom::Element; use xso::error::{Error, FromElementError}; #[cfg(target_pointer_width = "32")] #[test] fn test_size() { assert_size!(Replace, 12); } #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(Replace, 24); } #[test] fn test_simple() { let elem: Element = "" .parse() .unwrap(); Replace::try_from(elem).unwrap(); } #[cfg(not(feature = "disable-validation"))] #[test] fn test_invalid_attribute() { let elem: Element = "" .parse() .unwrap(); let error = Replace::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!(message, "Unknown attribute in Replace element."); } #[test] #[cfg_attr(feature = "disable-validation", should_panic = "Result::unwrap_err")] fn test_invalid_child() { let elem: Element = "" .parse() .unwrap(); let error = Replace::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!(message, "Unknown child in Replace element."); } #[test] fn test_invalid_id() { let elem: Element = "" .parse() .unwrap(); let error = Replace::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!( message, "Required attribute field 'id' on Replace element missing." ); } #[test] fn test_serialise() { let elem: Element = "" .parse() .unwrap(); let replace = Replace { id: Id(String::from("coucou")), }; let elem2 = replace.into(); assert_eq!(elem, elem2); } } xmpp-parsers-0.22.0/src/message_displayed.rs000064400000000000000000000012211046102023000171610ustar 00000000000000// Copyright (c) 2024 Emmanuel Gil Peyrot // // 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 xso::{AsXml, FromXml}; use crate::ns; use crate::stanza_id::StanzaId; /// Mention that a particular message has been displayed by at least one client. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::MDS, name = "displayed")] pub struct Displayed { /// Reference to the message having been displayed. #[xml(child)] pub stanza_id: StanzaId, } xmpp-parsers-0.22.0/src/mood.rs000064400000000000000000000253711046102023000144510ustar 00000000000000// Copyright (c) 2017 Emmanuel Gil Peyrot // // 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 xso::{AsXml, FromXml}; use crate::ns; /// Enum representing all of the possible values of the XEP-0107 moods. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::MOOD, exhaustive)] pub enum MoodEnum { /// Impressed with fear or apprehension; in fear; apprehensive. #[xml(name = "afraid")] Afraid, /// Astonished; confounded with fear, surprise or wonder. #[xml(name = "amazed")] Amazed, /// Inclined to love; having a propensity to love, or to sexual enjoyment; loving, fond, affectionate, passionate, lustful, sexual, etc. #[xml(name = "amorous")] Amorous, /// Displaying or feeling anger, i.e., a strong feeling of displeasure, hostility or antagonism towards someone or something, usually combined with an urge to harm. #[xml(name = "angry")] Angry, /// To be disturbed or irritated, especially by continued or repeated acts. #[xml(name = "annoyed")] Annoyed, /// Full of anxiety or disquietude; greatly concerned or solicitous, esp. respecting something future or unknown; being in painful suspense. #[xml(name = "anxious")] Anxious, /// To be stimulated in one's feelings, especially to be sexually stimulated. #[xml(name = "aroused")] Aroused, /// Feeling shame or guilt. #[xml(name = "ashamed")] Ashamed, /// Suffering from boredom; uninterested, without attention. #[xml(name = "bored")] Bored, /// Strong in the face of fear; courageous. #[xml(name = "brave")] Brave, /// Peaceful, quiet. #[xml(name = "calm")] Calm, /// Taking care or caution; tentative. #[xml(name = "cautious")] Cautious, /// Feeling the sensation of coldness, especially to the point of discomfort. #[xml(name = "cold")] Cold, /// Feeling very sure of or positive about something, especially about one's own capabilities. #[xml(name = "confident")] Confident, /// Chaotic, jumbled or muddled. #[xml(name = "confused")] Confused, /// Feeling introspective or thoughtful. #[xml(name = "contemplative")] Contemplative, /// Pleased at the satisfaction of a want or desire; satisfied. #[xml(name = "contented")] Contented, /// Grouchy, irritable; easily upset. #[xml(name = "cranky")] Cranky, /// Feeling out of control; feeling overly excited or enthusiastic. #[xml(name = "crazy")] Crazy, /// Feeling original, expressive, or imaginative. #[xml(name = "creative")] Creative, /// Inquisitive; tending to ask questions, investigate, or explore. #[xml(name = "curious")] Curious, /// Feeling sad and dispirited. #[xml(name = "dejected")] Dejected, /// Severely despondent and unhappy. #[xml(name = "depressed")] Depressed, /// Defeated of expectation or hope; let down. #[xml(name = "disappointed")] Disappointed, /// Filled with disgust; irritated and out of patience. #[xml(name = "disgusted")] Disgusted, /// Feeling a sudden or complete loss of courage in the face of trouble or danger. #[xml(name = "dismayed")] Dismayed, /// Having one's attention diverted; preoccupied. #[xml(name = "distracted")] Distracted, /// Having a feeling of shameful discomfort. #[xml(name = "embarrassed")] Embarrassed, /// Feeling pain by the excellence or good fortune of another. #[xml(name = "envious")] Envious, /// Having great enthusiasm. #[xml(name = "excited")] Excited, /// In the mood for flirting. #[xml(name = "flirtatious")] Flirtatious, /// Suffering from frustration; dissatisfied, agitated, or discontented because one is unable to perform an action or fulfill a desire. #[xml(name = "frustrated")] Frustrated, /// Feeling appreciation or thanks. #[xml(name = "grateful")] Grateful, /// Feeling very sad about something, especially something lost; mournful; sorrowful. #[xml(name = "grieving")] Grieving, /// Unhappy and irritable. #[xml(name = "grumpy")] Grumpy, /// Feeling responsible for wrongdoing; feeling blameworthy. #[xml(name = "guilty")] Guilty, /// Experiencing the effect of favourable fortune; having the feeling arising from the consciousness of well-being or of enjoyment; enjoying good of any kind, as peace, tranquillity, comfort; contented; joyous. #[xml(name = "happy")] Happy, /// Having a positive feeling, belief, or expectation that something wished for can or will happen. #[xml(name = "hopeful")] Hopeful, /// Feeling the sensation of heat, especially to the point of discomfort. #[xml(name = "hot")] Hot, /// Having or showing a modest or low estimate of one's own importance; feeling lowered in dignity or importance. #[xml(name = "humbled")] Humbled, /// Feeling deprived of dignity or self-respect. #[xml(name = "humiliated")] Humiliated, /// Having a physical need for food. #[xml(name = "hungry")] Hungry, /// Wounded, injured, or pained, whether physically or emotionally. #[xml(name = "hurt")] Hurt, /// Favourably affected by something or someone. #[xml(name = "impressed")] Impressed, /// Feeling amazement at something or someone; or feeling a combination of fear and reverence. #[xml(name = "in_awe")] InAwe, /// Feeling strong affection, care, liking, or attraction.. #[xml(name = "in_love")] InLove, /// Showing anger or indignation, especially at something unjust or wrong. #[xml(name = "indignant")] Indignant, /// Showing great attention to something or someone; having or showing interest. #[xml(name = "interested")] Interested, /// Under the influence of alcohol; drunk. #[xml(name = "intoxicated")] Intoxicated, /// Feeling as if one cannot be defeated, overcome or denied. #[xml(name = "invincible")] Invincible, /// Fearful of being replaced in position or affection. #[xml(name = "jealous")] Jealous, /// Feeling isolated, empty, or abandoned. #[xml(name = "lonely")] Lonely, /// Unable to find one's way, either physically or emotionally. #[xml(name = "lost")] Lost, /// Feeling as if one will be favored by luck. #[xml(name = "lucky")] Lucky, /// Causing or intending to cause intentional harm; bearing ill will towards another; cruel; malicious. #[xml(name = "mean")] Mean, /// Given to sudden or frequent changes of mind or feeling; temperamental. #[xml(name = "moody")] Moody, /// Easily agitated or alarmed; apprehensive or anxious. #[xml(name = "nervous")] Nervous, /// Not having a strong mood or emotional state. #[xml(name = "neutral")] Neutral, /// Feeling emotionally hurt, displeased, or insulted. #[xml(name = "offended")] Offended, /// Feeling resentful anger caused by an extremely violent or vicious attack, or by an offensive, immoral, or indecent act. #[xml(name = "outraged")] Outraged, /// Interested in play; fun, recreational, unserious, lighthearted; joking, silly. #[xml(name = "playful")] Playful, /// Feeling a sense of one's own worth or accomplishment. #[xml(name = "proud")] Proud, /// Having an easy-going mood; not stressed; calm. #[xml(name = "relaxed")] Relaxed, /// Feeling uplifted because of the removal of stress or discomfort. #[xml(name = "relieved")] Relieved, /// Feeling regret or sadness for doing something wrong. #[xml(name = "remorseful")] Remorseful, /// Without rest; unable to be still or quiet; uneasy; continually moving. #[xml(name = "restless")] Restless, /// Feeling sorrow; sorrowful, mournful. #[xml(name = "sad")] Sad, /// Mocking and ironical. #[xml(name = "sarcastic")] Sarcastic, /// Pleased at the fulfillment of a need or desire. #[xml(name = "satisfied")] Satisfied, /// Without humor or expression of happiness; grave in manner or disposition; earnest; thoughtful; solemn. #[xml(name = "serious")] Serious, /// Surprised, startled, confused, or taken aback. #[xml(name = "shocked")] Shocked, /// Feeling easily frightened or scared; timid; reserved or coy. #[xml(name = "shy")] Shy, /// Feeling in poor health; ill. #[xml(name = "sick")] Sick, /// Feeling the need for sleep. #[xml(name = "sleepy")] Sleepy, /// Acting without planning; natural; impulsive. #[xml(name = "spontaneous")] Spontaneous, /// Suffering emotional pressure. #[xml(name = "stressed")] Stressed, /// Capable of producing great physical force; or, emotionally forceful, able, determined, unyielding. #[xml(name = "strong")] Strong, /// Experiencing a feeling caused by something unexpected. #[xml(name = "surprised")] Surprised, /// Showing appreciation or gratitude. #[xml(name = "thankful")] Thankful, /// Feeling the need to drink. #[xml(name = "thirsty")] Thirsty, /// In need of rest or sleep. #[xml(name = "tired")] Tired, /// [Feeling any emotion not defined here.] #[xml(name = "undefined")] Undefined, /// Lacking in force or ability, either physical or emotional. #[xml(name = "weak")] Weak, /// Thinking about unpleasant things that have happened or that might happen; feeling afraid and unhappy. #[xml(name = "worried")] Worried, } generate_elem_id!( /// Free-form text description of the mood. Text, "text", MOOD ); #[cfg(test)] mod tests { use super::*; use minidom::Element; #[cfg(target_pointer_width = "32")] #[test] fn test_size() { assert_size!(MoodEnum, 1); assert_size!(Text, 12); } #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(MoodEnum, 1); assert_size!(Text, 24); } #[test] fn test_simple() { let elem: Element = "" .parse() .unwrap(); let mood = MoodEnum::try_from(elem).unwrap(); assert_eq!(mood, MoodEnum::Happy); } #[test] fn test_text() { let elem: Element = "Yay!" .parse() .unwrap(); let elem2 = elem.clone(); let text = Text::try_from(elem).unwrap(); assert_eq!(text.0, String::from("Yay!")); let elem3 = text.into(); assert_eq!(elem2, elem3); } } xmpp-parsers-0.22.0/src/muc/mod.rs000064400000000000000000000007541046102023000150540ustar 00000000000000// Copyright (c) 2017 Maxime “pep” Buquet // // 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/. /// The `http://jabber.org/protocol/muc` protocol. #[allow(clippy::module_inception)] pub mod muc; /// The `http://jabber.org/protocol/muc#user` protocol. pub mod user; pub use self::muc::Muc; pub use self::user::MucUser; xmpp-parsers-0.22.0/src/muc/muc.rs000064400000000000000000000141231046102023000150540ustar 00000000000000// Copyright (c) 2017 Maxime “pep” Buquet // Copyright (c) 2017 Emmanuel Gil Peyrot // // 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 xso::{AsXml, FromXml}; use crate::date::DateTime; use crate::ns; use crate::presence::PresencePayload; /// Represents the query for messages before our join. #[derive(FromXml, AsXml, PartialEq, Debug, Clone, Default)] #[xml(namespace = ns::MUC, name = "history")] pub struct History { /// How many characters of history to send, in XML characters. #[xml(attribute(default))] pub maxchars: Option, /// How many messages to send. #[xml(attribute(default))] pub maxstanzas: Option, /// Only send messages received in these last seconds. #[xml(attribute(default))] pub seconds: Option, /// Only send messages after this date. #[xml(attribute(default))] pub since: Option, } impl History { /// Create a new empty history element. pub fn new() -> Self { History::default() } /// Set how many characters of history to send. pub fn with_maxchars(mut self, maxchars: u32) -> Self { self.maxchars = Some(maxchars); self } /// Set how many messages to send. pub fn with_maxstanzas(mut self, maxstanzas: u32) -> Self { self.maxstanzas = Some(maxstanzas); self } /// Only send messages received in these last seconds. pub fn with_seconds(mut self, seconds: u32) -> Self { self.seconds = Some(seconds); self } /// Only send messages received since this date. pub fn with_since(mut self, since: DateTime) -> Self { self.since = Some(since); self } } /// Represents a room join request. #[derive(FromXml, AsXml, PartialEq, Debug, Clone, Default)] #[xml(namespace = ns::MUC, name = "x")] pub struct Muc { /// Password to use when the room is protected by a password. #[xml(extract(default, fields(text(type_ = String))))] pub password: Option, /// Controls how much and how old we want to receive history on join. #[xml(child(default))] pub history: Option, } impl PresencePayload for Muc {} impl Muc { /// Create a new MUC join element. pub fn new() -> Self { Muc::default() } /// Join a room with this password. pub fn with_password(mut self, password: String) -> Self { self.password = Some(password); self } /// Join a room with only that much history. pub fn with_history(mut self, history: History) -> Self { self.history = Some(history); self } } #[cfg(test)] mod tests { use super::*; use core::str::FromStr; use minidom::Element; use xso::error::{Error, FromElementError}; #[cfg(target_pointer_width = "32")] #[test] fn test_size() { assert_size!(History, 40); assert_size!(Muc, 52); } #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(History, 40); assert_size!(Muc, 64); } #[test] fn test_muc_simple() { let elem: Element = "" .parse() .unwrap(); Muc::try_from(elem).unwrap(); } #[test] #[cfg_attr(feature = "disable-validation", should_panic = "Result::unwrap_err")] fn test_muc_invalid_child() { let elem: Element = "" .parse() .unwrap(); let error = Muc::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!(message, "Unknown child in Muc element."); } #[test] fn test_muc_serialise() { let elem: Element = "" .parse() .unwrap(); let muc = Muc { password: None, history: None, }; let elem2 = muc.into(); assert_eq!(elem, elem2); } #[cfg(not(feature = "disable-validation"))] #[test] fn test_muc_invalid_attribute() { let elem: Element = "" .parse() .unwrap(); let error = Muc::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!(message, "Unknown attribute in Muc element."); } #[test] fn test_muc_simple_password() { let elem: Element = "coucou" .parse() .unwrap(); let elem1 = elem.clone(); let muc = Muc::try_from(elem).unwrap(); assert_eq!(muc.password, Some("coucou".to_owned())); let elem2 = Element::from(muc); assert_eq!(elem1, elem2); } #[test] fn history() { let elem: Element = " " .parse() .unwrap(); let muc = Muc::try_from(elem).unwrap(); let muc2 = Muc::new().with_history(History::new().with_maxstanzas(0)); assert_eq!(muc, muc2); let history = muc.history.unwrap(); assert_eq!(history.maxstanzas, Some(0)); assert_eq!(history.maxchars, None); assert_eq!(history.seconds, None); assert_eq!(history.since, None); let elem: Element = " " .parse() .unwrap(); let muc = Muc::try_from(elem).unwrap(); assert_eq!( muc.history.unwrap().since.unwrap(), DateTime::from_str("1970-01-01T00:00:00+00:00").unwrap() ); } } xmpp-parsers-0.22.0/src/muc/user.rs000064400000000000000000000630351046102023000152540ustar 00000000000000// Copyright (c) 2017 Maxime “pep” Buquet // Copyright (c) 2017 Emmanuel Gil Peyrot // // 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 xso::{AsXml, FromXml}; use crate::message::MessagePayload; use crate::ns; use crate::presence::PresencePayload; use jid::{FullJid, Jid}; generate_attribute_enum!( /// Lists all of the possible status codes used in MUC presences. Status, "status", MUC_USER, "code", { /// 100: Inform user that any occupant is allowed to see the user's full JID NonAnonymousRoom => 100, /// 101: Inform user that his or her affiliation changed while not in the room AffiliationChange => 101, /// 102: Inform occupants that room now shows unavailable members ConfigShowsUnavailableMembers => 102, /// 103: Inform occupants that room now does not show unavailable members ConfigHidesUnavailableMembers => 103, /// 104: Inform occupants that a non-privacy-related room configuration change has occurred ConfigNonPrivacyRelated => 104, /// 110: Inform user that presence refers to itself SelfPresence => 110, /// 170: Inform occupants that room logging is now enabled ConfigRoomLoggingEnabled => 170, /// 171: Inform occupants that room logging is now disabled ConfigRoomLoggingDisabled => 171, /// 172: Inform occupants that the room is now non-anonymous ConfigRoomNonAnonymous => 172, /// 173: Inform occupants that the room is now semi-anonymous ConfigRoomSemiAnonymous => 173, /// 201: Inform user that a new room has been created RoomHasBeenCreated => 201, /// 210: Inform user that service has assigned or modified occupant's roomnick AssignedNick => 210, /// 301: Inform user that they have been banned from the room Banned => 301, /// 303: Inform all occupants of new room nickname NewNick => 303, /// 307: Inform user that they have been kicked from the room Kicked => 307, /// 321: Inform user that they are being removed from the room /// because of an affiliation change RemovalFromRoom => 321, /// 322: Inform user that they are being removed from the room /// because the room has been changed to members-only and the /// user is not a member ConfigMembersOnly => 322, /// 332: Inform user that they are being removed from the room /// because the MUC service is being shut down ServiceShutdown => 332, /// 333: Inform user that they are being removed from the room for technical reasons ServiceErrorKick => 333, }); /// Optional \ element used in \ elements inside presence stanzas of type /// "unavailable" that are sent to users who are kick or banned, as well as within IQs for tracking /// purposes. -- CHANGELOG 0.17 (2002-10-23) /// /// Possesses a 'jid' and a 'nick' attribute, so that an action can be attributed either to a real /// JID or to a roomnick. -- CHANGELOG 1.25 (2012-02-08) #[derive(FromXml, AsXml, Debug, Clone, PartialEq)] #[xml(namespace = ns::MUC_USER, name = "actor")] pub struct Actor { /// The full JID associated with this user. #[xml(attribute(default))] jid: Option, /// The nickname of this user. #[xml(attribute(default))] nick: Option, } /// Used to continue a one-to-one discussion in a room, with more than one /// participant. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::MUC_USER, name = "continue")] pub struct Continue { /// The thread to continue in this room. #[xml(attribute(default))] pub thread: Option, } generate_elem_id!( /// A reason for inviting, declining, etc. a request. Reason, "reason", MUC_USER ); generate_attribute!( /// The affiliation of an entity with a room, which isn’t tied to its /// presence in it. Affiliation, "affiliation", { /// The user who created the room, or who got appointed by its creator /// to be their equal. Owner => "owner", /// A user who has been empowered by an owner to do administrative /// operations. Admin => "admin", /// A user who is whitelisted to speak in moderated rooms, or to join a /// member-only room. Member => "member", /// A user who has been banned from this room. Outcast => "outcast", /// A normal participant. None => "none", }, Default = None ); generate_attribute!( /// The current role of an entity in a room, it can be changed by an owner /// or an administrator but will be lost once they leave the room. Role, "role", { /// This user can kick other participants, as well as grant and revoke /// them voice. Moderator => "moderator", /// A user who can speak in this room. Participant => "participant", /// A user who cannot speak in this room, and must request voice before /// doing so. Visitor => "visitor", /// A user who is absent from the room. None => "none", }, Default = None ); /// An item representing a user in a room. #[derive(FromXml, AsXml, Debug, PartialEq, Clone)] #[xml(namespace = ns::MUC_USER, name = "item")] pub struct Item { /// The affiliation of this user with the room. #[xml(attribute)] pub affiliation: Affiliation, /// The real JID of this user, if you are allowed to see it. #[xml(attribute(default))] pub jid: Option, /// The current nickname of this user. #[xml(attribute(default))] pub nick: Option, /// The current role of this user. #[xml(attribute)] pub role: Role, /// The actor affected by this item. #[xml(child(default))] pub actor: Option, /// Whether this continues a one-to-one discussion. #[xml(child(default))] pub continue_: Option, /// A reason for this item. #[xml(child(default))] pub reason: Option, } impl Item { /// Creates a new item with the given affiliation and role. pub fn new(affiliation: Affiliation, role: Role) -> Item { Item { affiliation, role, jid: None, nick: None, actor: None, continue_: None, reason: None, } } /// Set a jid for this Item pub fn with_jid(mut self, jid: FullJid) -> Item { self.jid = Some(jid); self } /// Set a nick for this Item pub fn with_nick>(mut self, nick: S) -> Item { self.nick = Some(nick.into()); self } /// Set an actor for this Item pub fn with_actor(mut self, actor: Actor) -> Item { self.actor = Some(actor); self } /// Set a continue value for this Item pub fn with_continue>(mut self, continue_: S) -> Item { self.continue_ = Some(Continue { thread: Some(continue_.into()), }); self } /// Set a reason for this Item pub fn with_reason>(mut self, reason: S) -> Item { self.reason = Some(Reason(reason.into())); self } } /// Mediated invite #[derive(FromXml, AsXml, Debug, PartialEq, Clone)] #[xml(namespace = ns::MUC_USER, name = "invite")] pub struct Invite { /// Sender. /// /// This is only populated for invites which have been mediated by a MUC /// and sent to the invitee. #[xml(attribute(default))] pub from: Option, /// Recipient. /// /// This is only populated for requests to mediate an invite through a /// MUC, before forwarding it to the invitee. #[xml(attribute(default))] pub to: Option, /// The optional reason for the invite. #[xml(extract(name = "reason", default, fields(text(type_ = String))))] pub reason: Option, } /// Rejection of a mediated invite. #[derive(FromXml, AsXml, Debug, PartialEq, Clone)] #[xml(namespace = ns::MUC_USER, name = "decline")] pub struct Decline { /// Sender. /// /// This is only populated for rejections which have been mediated by a /// MUC and sent to the inviter. #[xml(attribute(default))] pub from: Option, /// Recipient. /// /// This is only populated for requests to decline an invite through a /// MUC, before forwarding it to the inviter. #[xml(attribute(default))] pub to: Option, /// The optional reason for the rejection. #[xml(extract(name = "reason", default, fields(text(type_ = String))))] pub reason: Option, } /// The main muc#user element. #[derive(FromXml, AsXml, Debug, PartialEq, Clone)] #[xml(namespace = ns::MUC_USER, name = "x")] pub struct MucUser { /// List of statuses applying to this item. #[xml(child(n = ..))] pub status: Vec, /// List of items. #[xml(child(n = ..))] pub items: Vec, /// A mediated invite #[xml(child(default))] pub invite: Option, /// A mediated invite rejection #[xml(child(default))] pub decline: Option, } impl Default for MucUser { fn default() -> Self { Self::new() } } impl MucUser { /// Creates an empty MucUser pub fn new() -> MucUser { MucUser { status: vec![], items: vec![], invite: None, decline: None, } } /// Set statuses for this MucUser pub fn with_statuses(mut self, status: Vec) -> MucUser { self.status = status; self } /// Set items for this MucUser pub fn with_items(mut self, items: Vec) -> MucUser { self.items = items; self } } impl MessagePayload for MucUser {} impl PresencePayload for MucUser {} #[cfg(test)] mod tests { use super::*; use crate::message::Message; use crate::presence::{Presence, Type as PresenceType}; use jid::Jid; use minidom::Element; use xso::error::{Error, FromElementError}; #[cfg(target_pointer_width = "32")] #[test] fn test_size() { assert_size!(Status, 1); assert_size!(Actor, 28); assert_size!(Continue, 12); assert_size!(Reason, 12); assert_size!(Affiliation, 1); assert_size!(Role, 1); assert_size!(Item, 84); assert_size!(Invite, 44); assert_size!(Decline, 44); assert_size!(MucUser, 112); } #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(Status, 1); assert_size!(Actor, 56); assert_size!(Continue, 24); assert_size!(Reason, 24); assert_size!(Affiliation, 1); assert_size!(Role, 1); assert_size!(Item, 168); assert_size!(Invite, 88); assert_size!(Decline, 88); assert_size!(MucUser, 224); } #[test] fn test_simple() { let elem: Element = "" .parse() .unwrap(); MucUser::try_from(elem).unwrap(); } #[test] fn statuses_and_items() { let elem: Element = " " .parse() .unwrap(); let muc_user = MucUser::try_from(elem).unwrap(); assert_eq!(muc_user.status.len(), 2); assert_eq!(muc_user.status[0], Status::AffiliationChange); assert_eq!(muc_user.status[1], Status::ConfigShowsUnavailableMembers); assert_eq!(muc_user.items.len(), 1); assert_eq!(muc_user.items[0].affiliation, Affiliation::Member); assert_eq!(muc_user.items[0].role, Role::Moderator); } #[test] #[cfg_attr(feature = "disable-validation", should_panic = "Result::unwrap_err")] fn test_invalid_child() { let elem: Element = " " .parse() .unwrap(); let error = MucUser::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!(message, "Unknown child in MucUser element."); } #[test] fn test_serialise() { let elem: Element = "" .parse() .unwrap(); let muc = MucUser { status: vec![], items: vec![], invite: None, decline: None, }; let elem2 = muc.into(); assert_eq!(elem, elem2); } #[cfg(not(feature = "disable-validation"))] #[test] fn test_invalid_attribute() { let elem: Element = "" .parse() .unwrap(); let error = MucUser::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!(message, "Unknown attribute in MucUser element."); } #[test] fn test_status_simple() { let elem: Element = "" .parse() .unwrap(); Status::try_from(elem).unwrap(); } #[test] fn test_status_invalid() { let elem: Element = "" .parse() .unwrap(); let error = Status::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!(message, "Required attribute 'code' missing."); } #[cfg(not(feature = "disable-validation"))] #[test] fn test_status_invalid_child() { let elem: Element = " " .parse() .unwrap(); let error = Status::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!(message, "Unknown child in status element."); } #[test] fn test_status_simple_code() { let elem: Element = "" .parse() .unwrap(); let status = Status::try_from(elem).unwrap(); assert_eq!(status, Status::Kicked); } #[test] fn test_status_invalid_code() { let elem: Element = "" .parse() .unwrap(); let error = Status::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!(message, "Invalid status code value."); } #[test] fn test_status_invalid_code2() { let elem: Element = "" .parse() .unwrap(); let error = Status::try_from(elem).unwrap_err(); let error = match error { FromElementError::Invalid(Error::TextParseError(error)) if error.is::() => { error } _ => panic!(), }; assert_eq!(error.to_string(), "invalid digit found in string"); } // This test is now ignored because we switched to a representation where we can’t currently // validate whether one of the required attributes is present or not. #[test] #[ignore] fn test_actor_required_attributes() { let elem: Element = "" .parse() .unwrap(); let error = Actor::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!(message, "Either 'jid' or 'nick' attribute is required."); } #[test] fn test_actor_jid() { let elem: Element = "" .parse() .unwrap(); let actor = Actor::try_from(elem).unwrap(); assert_eq!(actor.jid, Some("foo@bar/baz".parse::().unwrap())); assert_eq!(actor.nick, None); } #[test] fn test_actor_nick() { let elem: Element = "" .parse() .unwrap(); let actor = Actor::try_from(elem).unwrap(); assert_eq!(actor.nick, Some("baz".to_owned())); assert_eq!(actor.jid, None); } #[test] fn test_continue_simple() { let elem: Element = "" .parse() .unwrap(); Continue::try_from(elem).unwrap(); } #[test] fn test_continue_thread_attribute() { let elem: Element = "" .parse() .unwrap(); let continue_ = Continue::try_from(elem).unwrap(); assert_eq!(continue_.thread, Some("foo".to_owned())); } #[test] #[cfg_attr(feature = "disable-validation", should_panic = "Result::unwrap_err")] fn test_continue_invalid() { let elem: Element = "" .parse() .unwrap(); let continue_ = Continue::try_from(elem).unwrap_err(); let message = match continue_ { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!(message, "Unknown child in Continue element.".to_owned()); } #[test] fn test_reason_simple() { let elem: Element = "Reason" .parse() .unwrap(); let elem2 = elem.clone(); let reason = Reason::try_from(elem).unwrap(); assert_eq!(reason.0, "Reason".to_owned()); let elem3 = reason.into(); assert_eq!(elem2, elem3); } #[cfg(not(feature = "disable-validation"))] #[test] fn test_reason_invalid_attribute() { let elem: Element = "" .parse() .unwrap(); let error = Reason::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!(message, "Unknown attribute in Reason element.".to_owned()); } #[cfg(not(feature = "disable-validation"))] #[test] fn test_reason_invalid() { let elem: Element = " " .parse() .unwrap(); let error = Reason::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!(message, "Unknown child in Reason element.".to_owned()); } #[cfg(not(feature = "disable-validation"))] #[test] fn test_item_invalid_attr() { let elem: Element = "" .parse() .unwrap(); let error = Item::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!(message, "Unknown attribute in Item element.".to_owned()); } #[test] fn test_item_affiliation_role_attr() { let elem: Element = "" .parse() .unwrap(); Item::try_from(elem).unwrap(); } #[test] fn test_item_affiliation_role_invalid_attr() { let elem: Element = "" .parse() .unwrap(); let error = Item::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!( message, "Required attribute field 'role' on Item element missing.".to_owned() ); } #[test] fn test_item_nick_attr() { let elem: Element = "" .parse() .unwrap(); let item = Item::try_from(elem).unwrap(); match item { Item { nick, .. } => assert_eq!(nick, Some("foobar".to_owned())), } } #[test] fn test_item_affiliation_role_invalid_attr2() { let elem: Element = "" .parse() .unwrap(); let error = Item::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!( message, "Required attribute field 'affiliation' on Item element missing.".to_owned() ); } #[test] fn test_item_role_actor_child() { let elem: Element = " " .parse() .unwrap(); let item = Item::try_from(elem).unwrap(); let Item { actor, .. } = item; let actor = actor.unwrap(); assert_eq!(actor.nick, Some("foobar".to_owned())); assert_eq!(actor.jid, None); } #[test] fn test_item_role_continue_child() { let elem: Element = " " .parse() .unwrap(); let item = Item::try_from(elem).unwrap(); let continue_1 = Continue { thread: Some("foobar".to_owned()), }; match item { Item { continue_: Some(continue_2), .. } => assert_eq!(continue_2.thread, continue_1.thread), _ => panic!(), } } #[test] fn test_item_role_reason_child() { let elem: Element = " foobar " .parse() .unwrap(); let item = Item::try_from(elem).unwrap(); match item { Item { reason, .. } => assert_eq!(reason, Some(Reason("foobar".to_owned()))), } } #[test] fn test_serialize_item() { let reference: Element = "foobar" .parse() .unwrap(); let elem: Element = "" .parse() .unwrap(); let actor = Actor::try_from(elem).unwrap(); let elem: Element = "" .parse() .unwrap(); let continue_ = Continue::try_from(elem).unwrap(); let elem: Element = "foobar" .parse() .unwrap(); let reason = Reason::try_from(elem).unwrap(); let item = Item { affiliation: Affiliation::Member, role: Role::Moderator, jid: None, nick: None, actor: Some(actor), reason: Some(reason), continue_: Some(continue_), }; let serialized: Element = item.into(); assert_eq!(serialized, reference); } #[test] fn presence_payload() { let elem: Element = "" .parse() .unwrap(); let presence = Presence::new(PresenceType::None).with_payloads(vec![elem]); assert_eq!(presence.payloads.len(), 1); } #[test] fn message_payload() { let jid: Jid = Jid::new("louise@example.com").unwrap(); let elem: Element = "" .parse() .unwrap(); let message = Message::new(jid).with_payloads(vec![elem]); assert_eq!(message.payloads.len(), 1); } } xmpp-parsers-0.22.0/src/nick.rs000064400000000000000000000044601046102023000144330ustar 00000000000000// Copyright (c) 2018 Emmanuel Gil Peyrot // // 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/. generate_elem_id!( /// Represents a global, memorable, friendly or informal name chosen by a user. Nick, "nick", NICK ); #[cfg(test)] mod tests { use super::*; use minidom::Element; #[cfg(not(feature = "disable-validation"))] use xso::error::{Error, FromElementError}; #[cfg(target_pointer_width = "32")] #[test] fn test_size() { assert_size!(Nick, 12); } #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(Nick, 24); } #[test] fn test_simple() { let elem: Element = "Link Mauve" .parse() .unwrap(); let nick = Nick::try_from(elem).unwrap(); assert_eq!(&nick.0, "Link Mauve"); } #[test] fn test_serialise() { let elem1 = Element::from(Nick(String::from("Link Mauve"))); let elem2: Element = "Link Mauve" .parse() .unwrap(); assert_eq!(elem1, elem2); } #[cfg(not(feature = "disable-validation"))] #[test] fn test_invalid() { let elem: Element = "" .parse() .unwrap(); let error = Nick::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!(message, "Unknown child in Nick element."); } #[cfg(not(feature = "disable-validation"))] #[test] fn test_invalid_attribute() { let elem: Element = "" .parse() .unwrap(); let error = Nick::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!(message, "Unknown attribute in Nick element."); } } xmpp-parsers-0.22.0/src/ns.rs000064400000000000000000000272341046102023000141330ustar 00000000000000// Copyright (c) 2017-2018 Emmanuel Gil Peyrot // Copyright (c) 2017 Maxime “pep” Buquet // // 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/. /// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core pub const JABBER_CLIENT: &str = "jabber:client"; /// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core pub const XMPP_STANZAS: &str = "urn:ietf:params:xml:ns:xmpp-stanzas"; /// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core pub const XMPP_STREAMS: &str = "urn:ietf:params:xml:ns:xmpp-streams"; /// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core pub const STREAM: &str = "http://etherx.jabber.org/streams"; /// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core pub const TLS: &str = "urn:ietf:params:xml:ns:xmpp-tls"; /// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core pub const SASL: &str = "urn:ietf:params:xml:ns:xmpp-sasl"; /// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core pub const BIND: &str = "urn:ietf:params:xml:ns:xmpp-bind"; /// RFC 6121: Extensible Messaging and Presence Protocol (XMPP): Instant Messaging and Presence pub const ROSTER: &str = "jabber:iq:roster"; /// RFC 7395: An Extensible Messaging and Presence Protocol (XMPP) Subprotocol for WebSocket pub const WEBSOCKET: &str = "urn:ietf:params:xml:ns:xmpp-framing"; /// XEP-0004: Data Forms pub const DATA_FORMS: &str = "jabber:x:data"; /// XEP-0030: Service Discovery pub const DISCO_INFO: &str = "http://jabber.org/protocol/disco#info"; /// XEP-0030: Service Discovery pub const DISCO_ITEMS: &str = "http://jabber.org/protocol/disco#items"; /// XEP-0045: Multi-User Chat pub const MUC: &str = "http://jabber.org/protocol/muc"; /// XEP-0045: Multi-User Chat pub const MUC_USER: &str = "http://jabber.org/protocol/muc#user"; /// XEP-0047: In-Band Bytestreams pub const IBB: &str = "http://jabber.org/protocol/ibb"; /// XEP-0048: Bookmarks pub const BOOKMARKS: &str = "storage:bookmarks"; /// XEP-0049: Private XML Storage pub const PRIVATE: &str = "jabber:iq:private"; /// XEP-0054: vcard-temp pub const VCARD: &str = "vcard-temp"; /// XEP-0059: Result Set Management pub const RSM: &str = "http://jabber.org/protocol/rsm"; /// XEP-0060: Publish-Subscribe pub const PUBSUB: &str = "http://jabber.org/protocol/pubsub"; /// XEP-0060: Publish-Subscribe pub const PUBSUB_ERRORS: &str = "http://jabber.org/protocol/pubsub#errors"; /// XEP-0060: Publish-Subscribe pub const PUBSUB_EVENT: &str = "http://jabber.org/protocol/pubsub#event"; /// XEP-0060: Publish-Subscribe pub const PUBSUB_OWNER: &str = "http://jabber.org/protocol/pubsub#owner"; /// XEP-0060: Publish-Subscribe node configuration pub const PUBSUB_CONFIGURE: &str = "http://jabber.org/protocol/pubsub#node_config"; /// XEP-0066: Out of Band Data pub const OOB: &str = "jabber:x:oob"; /// XEP-0070: Verifying HTTP Requests via XMPP pub const HTTP_AUTH: &str = "http://jabber.org/protocol/http-auth"; /// XEP-0071: XHTML-IM pub const XHTML_IM: &str = "http://jabber.org/protocol/xhtml-im"; /// XEP-0071: XHTML-IM pub const XHTML: &str = "http://www.w3.org/1999/xhtml"; /// XEP-0077: In-Band Registration pub const REGISTER: &str = "jabber:iq:register"; /// XEP-0084: User Avatar pub const AVATAR_DATA: &str = "urn:xmpp:avatar:data"; /// XEP-0084: User Avatar pub const AVATAR_METADATA: &str = "urn:xmpp:avatar:metadata"; /// XEP-0085: Chat State Notifications pub const CHATSTATES: &str = "http://jabber.org/protocol/chatstates"; /// XEP-0092: Software Version pub const VERSION: &str = "jabber:iq:version"; /// XEP-0107: User Mood pub const MOOD: &str = "http://jabber.org/protocol/mood"; /// XEP-0114: Jabber Component Protocol pub const COMPONENT_ACCEPT: &str = "jabber:component:accept"; /// XEP-0114: Jabber Component Protocol pub const COMPONENT: &str = "jabber:component:accept"; /// XEP-0115: Entity Capabilities pub const CAPS: &str = "http://jabber.org/protocol/caps"; /// XEP-0118: User Tune pub const TUNE: &str = "http://jabber.org/protocol/tune"; /// XEP-0122: Data Forms Validation pub const XDATA_VALIDATE: &str = "http://jabber.org/protocol/xdata-validate"; /// XEP-0153: vCard-Based Avatars pub const VCARD_UPDATE: &str = "vcard-temp:x:update"; /// XEP-0157: Contact Addresses for XMPP Services pub const SERVER_INFO: &str = "http://jabber.org/network/serverinfo"; /// XEP-0166: Jingle pub const JINGLE: &str = "urn:xmpp:jingle:1"; /// XEP-0167: Jingle RTP Sessions pub const JINGLE_RTP: &str = "urn:xmpp:jingle:apps:rtp:1"; /// XEP-0167: Jingle RTP Sessions pub const JINGLE_RTP_AUDIO: &str = "urn:xmpp:jingle:apps:rtp:audio"; /// XEP-0167: Jingle RTP Sessions pub const JINGLE_RTP_VIDEO: &str = "urn:xmpp:jingle:apps:rtp:video"; /// XEP-0172: User Nickname pub const NICK: &str = "http://jabber.org/protocol/nick"; /// XEP-0176: Jingle ICE-UDP Transport Method pub const JINGLE_ICE_UDP: &str = "urn:xmpp:jingle:transports:ice-udp:1"; /// XEP-0177: Jingle Raw UDP Transport Method pub const JINGLE_RAW_UDP: &str = "urn:xmpp:jingle:transports:raw-udp:1"; /// XEP-0184: Message Delivery Receipts pub const RECEIPTS: &str = "urn:xmpp:receipts"; /// XEP-0191: Blocking Command pub const BLOCKING: &str = "urn:xmpp:blocking"; /// XEP-0191: Blocking Command pub const BLOCKING_ERRORS: &str = "urn:xmpp:blocking:errors"; /// XEP-0198: Stream Management pub const SM: &str = "urn:xmpp:sm:3"; /// XEP-0199: XMPP Ping pub const PING: &str = "urn:xmpp:ping"; /// XEP-0202: Entity Time pub const TIME: &str = "urn:xmpp:time"; /// XEP-0203: Delayed Delivery pub const DELAY: &str = "urn:xmpp:delay"; /// XEP-0215: External Service Discovery pub const EXT_DISCO: &str = "urn:xmpp:extdisco:2"; /// XEP-0221: Data Forms Media Element pub const MEDIA_ELEMENT: &str = "urn:xmpp:media-element"; /// XEP-0224: Attention pub const ATTENTION: &str = "urn:xmpp:attention:0"; /// XEP-0231: Bits of Binary pub const BOB: &str = "urn:xmpp:bob"; /// XEP-0234: Jingle File Transfer pub const JINGLE_FT: &str = "urn:xmpp:jingle:apps:file-transfer:5"; /// XEP-0234: Jingle File Transfer pub const JINGLE_FT_ERROR: &str = "urn:xmpp:jingle:apps:file-transfer:errors:0"; /// XEP-0257: Client Certificate Management for SASL EXTERNAL pub const SASL_CERT: &str = "urn:xmpp:saslcert:1"; /// XEP-0260: Jingle SOCKS5 Bytestreams Transport Method pub const JINGLE_S5B: &str = "urn:xmpp:jingle:transports:s5b:1"; /// XEP-0261: Jingle In-Band Bytestreams Transport Method pub const JINGLE_IBB: &str = "urn:xmpp:jingle:transports:ibb:1"; /// XEP-0264: Jingle Content Thumbnails pub const JINGLE_THUMBNAILS: &str = "urn:xmpp:thumbs:1"; /// XEP-0277: Microblogging over XMPP pub const MICROBLOG: &str = "urn:xmpp:microblog:0"; /// XEP-0280: Message Carbons pub const CARBONS: &str = "urn:xmpp:carbons:2"; /// XEP-0293: Jingle RTP Feedback Negotiation pub const JINGLE_RTCP_FB: &str = "urn:xmpp:jingle:apps:rtp:rtcp-fb:0"; /// XEP-0294: Jingle RTP Header Extensions Negotiation pub const JINGLE_RTP_HDREXT: &str = "urn:xmpp:jingle:apps:rtp:rtp-hdrext:0"; /// XEP-0297: Stanza Forwarding pub const FORWARD: &str = "urn:xmpp:forward:0"; /// XEP-0300: Use of Cryptographic Hash Functions in XMPP pub const HASHES: &str = "urn:xmpp:hashes:2"; /// XEP-0300: Use of Cryptographic Hash Functions in XMPP pub const HASH_ALGO_SHA_256: &str = "urn:xmpp:hash-function-text-names:sha-256"; /// XEP-0300: Use of Cryptographic Hash Functions in XMPP pub const HASH_ALGO_SHA_512: &str = "urn:xmpp:hash-function-text-names:sha-512"; /// XEP-0300: Use of Cryptographic Hash Functions in XMPP pub const HASH_ALGO_SHA3_256: &str = "urn:xmpp:hash-function-text-names:sha3-256"; /// XEP-0300: Use of Cryptographic Hash Functions in XMPP pub const HASH_ALGO_SHA3_512: &str = "urn:xmpp:hash-function-text-names:sha3-512"; /// XEP-0300: Use of Cryptographic Hash Functions in XMPP pub const HASH_ALGO_BLAKE2B_256: &str = "urn:xmpp:hash-function-text-names:id-blake2b256"; /// XEP-0300: Use of Cryptographic Hash Functions in XMPP pub const HASH_ALGO_BLAKE2B_512: &str = "urn:xmpp:hash-function-text-names:id-blake2b512"; /// XEP-0301: In-Band Real Time Text pub const RTT: &str = "urn:xmpp:rtt:0"; /// XEP-0308: Last Message Correction pub const MESSAGE_CORRECT: &str = "urn:xmpp:message-correct:0"; /// XEP-0313: Message Archive Management pub const MAM: &str = "urn:xmpp:mam:2"; /// XEP-0319: Last User Interaction in Presence pub const IDLE: &str = "urn:xmpp:idle:1"; /// XEP-0320: Use of DTLS-SRTP in Jingle Sessions pub const JINGLE_DTLS: &str = "urn:xmpp:jingle:apps:dtls:0"; /// XEP-0328: JID Prep pub const JID_PREP: &str = "urn:xmpp:jidprep:0"; /// XEP-0335: JSON Containers pub const JSON_CONTAINERS: &str = "urn:xmpp:json:0"; /// XEP-0338: Jingle Grouping Framework pub const JINGLE_GROUPING: &str = "urn:xmpp:jingle:apps:grouping:0"; /// XEP-0339: Source-Specific Media Attributes in Jingle pub const JINGLE_SSMA: &str = "urn:xmpp:jingle:apps:rtp:ssma:0"; /// XEP-0352: Client State Indication pub const CSI: &str = "urn:xmpp:csi:0"; /// XEP-0353: Jingle Message Initiation pub const JINGLE_MESSAGE: &str = "urn:xmpp:jingle-message:0"; /// XEP-0357: Push Notifications pub const PUSH: &str = "urn:xmpp:push:0"; /// XEP-0359: Unique and Stable Stanza IDs pub const SID: &str = "urn:xmpp:sid:0"; /// XEP-0363: HTTP File Upload pub const HTTP_UPLOAD: &str = "urn:xmpp:http:upload:0"; /// XEP-0373: OpenPGP for XMPP pub const OX: &str = "urn:xmpp:openpgp:0"; /// XEP-0373: OpenPGP for XMPP pub const OX_PUBKEYS: &str = "urn:xmpp:openpgp:0:public-keys"; /// XEP-0377: Spam Reporting pub const SPAM_REPORTING: &str = "urn:xmpp:reporting:1"; /// XEP-0380: Explicit Message Encryption pub const EME: &str = "urn:xmpp:eme:0"; /// XEP-0384: OMEMO Encryption (experimental version 0.3.0) pub const LEGACY_OMEMO: &str = "eu.siacs.conversations.axolotl"; /// XEP-0384: OMEMO Encryption (experimental version 0.3.0) pub const LEGACY_OMEMO_DEVICELIST: &str = "eu.siacs.conversations.axolotl.devicelist"; /// XEP-0384: OMEMO Encryption (experimental version 0.3.0) pub const LEGACY_OMEMO_BUNDLES: &str = "eu.siacs.conversations.axolotl.bundles"; /// XEP-0386: Bind 2 pub const BIND2: &str = "urn:xmpp:bind:0"; /// XEP-0388: Extensible SASL Profile pub const SASL2: &str = "urn:xmpp:sasl:2"; /// XEP-0390: Entity Capabilities 2.0 pub const ECAPS2: &str = "urn:xmpp:caps"; /// XEP-0390: Entity Capabilities 2.0 pub const ECAPS2_OPTIMIZE: &str = "urn:xmpp:caps:optimize"; /// XEP-0402: PEP Native Bookmarks pub const BOOKMARKS2: &str = "urn:xmpp:bookmarks:1"; /// XEP-0402: PEP Native Bookmarks pub const BOOKMARKS2_COMPAT: &str = "urn:xmpp:bookmarks:1#compat"; /// XEP-0402: PEP Native Bookmarks pub const BOOKMARKS2_COMPAT_PEP: &str = "urn:xmpp:bookmarks:1#compat-pep"; /// XEP-0421: Anonymous unique occupant identifiers for MUCs pub const OID: &str = "urn:xmpp:occupant-id:0"; /// XEP-0440: SASL Channel-Binding Type Capability pub const SASL_CB: &str = "urn:xmpp:sasl-cb:0"; /// XEP-0444: Message Reactions pub const REACTIONS: &str = "urn:xmpp:reactions:0"; /// XEP-0478: Stream Limits Advertisement pub const STREAM_LIMITS: &str = "urn:xmpp:stream-limits:0"; /// XEP-0484: Fast Authentication Streamlining Tokens pub const FAST: &str = "urn:xmpp:fast:0"; /// XEP-0490: Message Displayed Synchronization pub const MDS: &str = "urn:xmpp:mds:displayed:0"; /// Alias for the main namespace of the stream, that is "jabber:client" when /// the component feature isn’t enabled. #[cfg(not(feature = "component"))] pub const DEFAULT_NS: &str = JABBER_CLIENT; /// Alias for the main namespace of the stream, that is /// "jabber:component:accept" when the component feature is enabled. #[cfg(feature = "component")] pub const DEFAULT_NS: &str = COMPONENT_ACCEPT; xmpp-parsers-0.22.0/src/occupant_id.rs000064400000000000000000000056111046102023000157760ustar 00000000000000// Copyright (c) 2019 Emmanuel Gil Peyrot // // 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 xso::{AsXml, FromXml}; use crate::message::MessagePayload; use crate::ns; use crate::presence::PresencePayload; /// Unique identifier given to a MUC participant. /// /// It allows clients to identify a MUC participant across reconnects and /// renames. It thus prevents impersonification of anonymous users. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::OID, name = "occupant-id")] pub struct OccupantId { /// The id associated to the sending user by the MUC service. #[xml(attribute)] pub id: String, } impl MessagePayload for OccupantId {} impl PresencePayload for OccupantId {} #[cfg(test)] mod tests { use super::*; use minidom::Element; use xso::error::{Error, FromElementError}; #[cfg(target_pointer_width = "32")] #[test] fn test_size() { assert_size!(OccupantId, 12); } #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(OccupantId, 24); } #[test] fn test_simple() { let elem: Element = "" .parse() .unwrap(); let origin_id = OccupantId::try_from(elem).unwrap(); assert_eq!(origin_id.id, "coucou"); } #[test] #[cfg_attr(feature = "disable-validation", should_panic = "Result::unwrap_err")] fn test_invalid_child() { let elem: Element = "" .parse() .unwrap(); let error = OccupantId::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!(message, "Unknown child in OccupantId element."); } #[test] fn test_invalid_id() { let elem: Element = "" .parse() .unwrap(); let error = OccupantId::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!( message, "Required attribute field 'id' on OccupantId element missing." ); } #[test] fn test_serialise() { let elem: Element = "" .parse() .unwrap(); let occupant_id = OccupantId { id: String::from("coucou"), }; let elem2 = occupant_id.into(); assert_eq!(elem, elem2); } } xmpp-parsers-0.22.0/src/oob.rs000064400000000000000000000036701046102023000142700ustar 00000000000000// Copyright (c) 2024 Paul Fariello // // 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 xso::{AsXml, FromXml}; use crate::message::MessagePayload; use crate::ns; /// Defines associated out of band url. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::OOB, name = "x")] pub struct Oob { /// The associated URL. #[xml(extract(fields(text)))] pub url: String, /// An optional description of the out of band data. #[xml(extract(default, fields(text(type_ = String))))] pub desc: Option, } impl MessagePayload for Oob {} #[cfg(test)] mod tests { use super::*; use minidom::Element; use xso::error::{Error, FromElementError}; #[cfg(target_pointer_width = "32")] #[test] fn test_size() { assert_size!(Oob, 24); } #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(Oob, 48); } #[test] fn test_simple() { let elem: Element = "http://example.org" .parse() .unwrap(); Oob::try_from(elem).unwrap(); } #[test] fn test_with_desc() { let elem: Element = "http://example.orgExample website" .parse() .unwrap(); Oob::try_from(elem).unwrap(); } #[test] fn test_invalid_child() { let elem: Element = "".parse().unwrap(); let error = Oob::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!(message, "Missing child field 'url' in Oob element."); } } xmpp-parsers-0.22.0/src/openpgp.rs000064400000000000000000000064351046102023000151630ustar 00000000000000// Copyright (c) 2019 Maxime “pep” Buquet // // 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 xso::{text::Base64, AsXml, FromXml}; use crate::date::DateTime; use crate::ns; use crate::pubsub::PubSubPayload; /// Data contained in the PubKey element // TODO: Merge this container with the PubKey struct #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::OX, name = "data")] pub struct PubKeyData { /// Base64 data #[xml(text = Base64)] pub data: Vec, } /// Pubkey element to be used in PubSub publish payloads. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::OX, name = "pubkey")] pub struct PubKey { /// Last updated date #[xml(attribute(default))] pub date: Option, /// Public key as base64 data #[xml(child)] pub data: PubKeyData, } impl PubSubPayload for PubKey {} /// Public key metadata #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::OX, name = "pubkey-metadata")] pub struct PubKeyMeta { /// OpenPGP v4 fingerprint #[xml(attribute = "v4-fingerprint")] pub v4fingerprint: String, /// Time the key was published or updated #[xml(attribute = "date")] pub date: DateTime, } /// List of public key metadata #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::OX, name = "public-key-list")] pub struct PubKeysMeta { /// Public keys #[xml(child(n = ..))] pub pubkeys: Vec, } impl PubSubPayload for PubKeysMeta {} #[cfg(test)] mod tests { use super::*; use crate::ns; use crate::pubsub::{ pubsub::{Item, Publish}, NodeName, }; use core::str::FromStr; use minidom::Element; #[test] fn pubsub_publish_pubkey_data() { let pubkey = PubKey { date: None, data: PubKeyData { data: (&"Foo").as_bytes().to_vec(), }, }; println!("Foo1: {:?}", pubkey); let pubsub = Publish { node: NodeName(format!("{}:{}", ns::OX_PUBKEYS, "some-fingerprint")), items: vec![Item::new(None, None, Some(pubkey))], }; println!("Foo2: {:?}", pubsub); } #[test] fn pubsub_publish_pubkey_meta() { let pubkeymeta = PubKeysMeta { pubkeys: vec![PubKeyMeta { v4fingerprint: "some-fingerprint".to_owned(), date: DateTime::from_str("2019-03-30T18:30:25Z").unwrap(), }], }; println!("Foo1: {:?}", pubkeymeta); let pubsub = Publish { node: NodeName("foo".to_owned()), items: vec![Item::new(None, None, Some(pubkeymeta))], }; println!("Foo2: {:?}", pubsub); } #[test] fn test_serialize_pubkey() { let reference: Element = "AAAA" .parse() .unwrap(); let pubkey = PubKey { date: None, data: PubKeyData { data: b"\0\0\0".to_vec(), }, }; let serialized: Element = pubkey.into(); assert_eq!(serialized, reference); } } xmpp-parsers-0.22.0/src/ping.rs000064400000000000000000000042071046102023000144430ustar 00000000000000// Copyright (c) 2017 Emmanuel Gil Peyrot // Copyright (c) 2017 Maxime “pep” Buquet // // 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 xso::{AsXml, FromXml}; use crate::iq::IqGetPayload; use crate::ns; /// Represents a ping to the recipient, which must be answered with an /// empty `` or with an error. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::PING, name = "ping")] pub struct Ping; impl IqGetPayload for Ping {} #[cfg(test)] mod tests { use super::*; use minidom::Element; #[cfg(not(feature = "disable-validation"))] use xso::error::{Error, FromElementError}; #[test] fn test_size() { assert_size!(Ping, 0); } #[test] fn test_simple() { let elem: Element = "".parse().unwrap(); Ping::try_from(elem).unwrap(); } #[test] fn test_serialise() { let elem1 = Element::from(Ping); let elem2: Element = "".parse().unwrap(); assert_eq!(elem1, elem2); } #[cfg(not(feature = "disable-validation"))] #[test] fn test_invalid() { let elem: Element = "" .parse() .unwrap(); let error = Ping::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!(message, "Unknown child in Ping element."); } #[cfg(not(feature = "disable-validation"))] #[test] fn test_invalid_attribute() { let elem: Element = "".parse().unwrap(); let error = Ping::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!(message, "Unknown attribute in Ping element."); } } xmpp-parsers-0.22.0/src/presence.rs000064400000000000000000000530511046102023000153130ustar 00000000000000// Copyright (c) 2017 Emmanuel Gil Peyrot // Copyright (c) 2017 Maxime “pep” Buquet // // 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 xso::{error::Error, AsOptionalXmlText, AsXml, AsXmlText, FromXml, FromXmlText}; use crate::message::Lang; use crate::ns; use alloc::{borrow::Cow, collections::BTreeMap}; use jid::Jid; use minidom::Element; /// Should be implemented on every known payload of a ``. pub trait PresencePayload: TryFrom + Into {} /// Specifies the availability of an entity or resource. #[derive(Debug, Clone, PartialEq)] pub enum Show { /// The entity or resource is temporarily away. Away, /// The entity or resource is actively interested in chatting. Chat, /// The entity or resource is busy (dnd = "Do Not Disturb"). Dnd, /// The entity or resource is away for an extended period (xa = "eXtended /// Away"). Xa, } impl FromXmlText for Show { fn from_xml_text(s: String) -> Result { Ok(match s.as_ref() { "away" => Show::Away, "chat" => Show::Chat, "dnd" => Show::Dnd, "xa" => Show::Xa, _ => return Err(Error::Other("Invalid value for show.")), }) } } impl AsXmlText for Show { fn as_xml_text(&self) -> Result, Error> { Ok(Cow::Borrowed(match self { Show::Away => "away", Show::Chat => "chat", Show::Dnd => "dnd", Show::Xa => "xa", })) } } type Status = String; /// Priority of this presence. This value can go from -128 to 127, defaults to /// 0, and any negative value will prevent this resource from receiving /// messages addressed to the bare JID. #[derive(FromXml, AsXml, Debug, Default, Clone, PartialEq)] #[xml(namespace = ns::DEFAULT_NS, name = "priority")] pub struct Priority(#[xml(text)] i8); /// Accepted values for the 'type' attribute of a presence. #[derive(Debug, Default, Clone, PartialEq)] pub enum Type { /// This value is not an acceptable 'type' attribute, it is only used /// internally to signal the absence of 'type'. #[default] None, /// An error has occurred regarding processing of a previously sent /// presence stanza; if the presence stanza is of type "error", it MUST /// include an \ child element (refer to /// [XMPP‑CORE](https://xmpp.org/rfcs/rfc6120.html)). Error, /// A request for an entity's current presence; SHOULD be generated only by /// a server on behalf of a user. Probe, /// The sender wishes to subscribe to the recipient's presence. Subscribe, /// The sender has allowed the recipient to receive their presence. Subscribed, /// The sender is no longer available for communication. Unavailable, /// The sender is unsubscribing from the receiver's presence. Unsubscribe, /// The subscription request has been denied or a previously granted /// subscription has been canceled. Unsubscribed, } impl FromXmlText for Type { fn from_xml_text(s: String) -> Result { Ok(match s.as_ref() { "error" => Type::Error, "probe" => Type::Probe, "subscribe" => Type::Subscribe, "subscribed" => Type::Subscribed, "unavailable" => Type::Unavailable, "unsubscribe" => Type::Unsubscribe, "unsubscribed" => Type::Unsubscribed, _ => { return Err(Error::Other( "Invalid 'type' attribute on presence element.", )); } }) } } impl AsOptionalXmlText for Type { fn as_optional_xml_text(&self) -> Result>, Error> { Ok(Some(Cow::Borrowed(match self { Type::None => return Ok(None), Type::Error => "error", Type::Probe => "probe", Type::Subscribe => "subscribe", Type::Subscribed => "subscribed", Type::Unavailable => "unavailable", Type::Unsubscribe => "unsubscribe", Type::Unsubscribed => "unsubscribed", }))) } } /// The main structure representing the `` stanza. #[derive(FromXml, AsXml, Debug, Clone, PartialEq)] #[xml(namespace = ns::DEFAULT_NS, name = "presence")] pub struct Presence { /// The sender of this presence. #[xml(attribute(default))] pub from: Option, /// The recipient of this presence. #[xml(attribute(default))] pub to: Option, /// The identifier, unique on this stream, of this stanza. #[xml(attribute(default))] pub id: Option, /// The type of this presence stanza. #[xml(attribute(default, name = "type"))] pub type_: Type, /// The availability of the sender of this presence. #[xml(extract(name = "show", default, fields(text(type_ = Show))))] pub show: Option, /// A localised list of statuses defined in this presence. #[xml(extract(n = .., name = "status", fields( lang(type_ = Lang, default), text(type_ = String), )))] pub statuses: BTreeMap, /// The sender’s resource priority, if negative it won’t receive messages /// that haven’t been directed to it. #[xml(child(default))] pub priority: Priority, /// A list of payloads contained in this presence. #[xml(element(n = ..))] pub payloads: Vec, } impl Presence { /// Create a new presence of this type. pub fn new(type_: Type) -> Presence { Presence { from: None, to: None, id: None, type_, show: None, statuses: BTreeMap::new(), priority: Priority(0i8), payloads: vec![], } } /// Create a presence without a type, which means available pub fn available() -> Presence { Self::new(Type::None) } /// Builds a presence of type Error pub fn error() -> Presence { Self::new(Type::Error) } /// Builds a presence of type Probe pub fn probe() -> Presence { Self::new(Type::Probe) } /// Builds a presence of type Subscribe pub fn subscribe() -> Presence { Self::new(Type::Subscribe) } /// Builds a presence of type Subscribed pub fn subscribed() -> Presence { Self::new(Type::Subscribed) } /// Builds a presence of type Unavailable pub fn unavailable() -> Presence { Self::new(Type::Unavailable) } /// Builds a presence of type Unsubscribe pub fn unsubscribe() -> Presence { Self::new(Type::Unsubscribe) } /// Set the emitter of this presence, this should only be useful for /// servers and components, as clients can only send presences from their /// own resource (which is implicit). pub fn with_from>(mut self, from: J) -> Presence { self.from = Some(from.into()); self } /// Set the recipient of this presence, this is only useful for directed /// presences. pub fn with_to>(mut self, to: J) -> Presence { self.to = Some(to.into()); self } /// Set the identifier for this presence. pub fn with_id(mut self, id: String) -> Presence { self.id = Some(id); self } /// Set the availability information of this presence. pub fn with_show(mut self, show: Show) -> Presence { self.show = Some(show); self } /// Set the priority of this presence. pub fn with_priority(mut self, priority: i8) -> Presence { self.priority = Priority(priority); self } /// Set a payload inside this presence. pub fn with_payload(mut self, payload: P) -> Presence { self.payloads.push(payload.into()); self } /// Set the payloads of this presence. pub fn with_payloads(mut self, payloads: Vec) -> Presence { self.payloads = payloads; self } /// Set the availability information of this presence. pub fn set_status(&mut self, lang: L, status: S) where L: Into, S: Into, { self.statuses.insert(lang.into(), status.into()); } /// Add a payload to this presence. pub fn add_payload(&mut self, payload: P) { self.payloads.push(payload.into()); } } #[cfg(test)] mod tests { use super::*; use jid::{BareJid, FullJid}; use xso::error::FromElementError; use xso::exports::rxml; #[cfg(target_pointer_width = "32")] #[test] fn test_size() { assert_size!(Show, 1); assert_size!(Type, 1); assert_size!(Presence, 72); } #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(Show, 1); assert_size!(Type, 1); assert_size!(Presence, 144); } #[test] fn test_simple() { #[cfg(not(feature = "component"))] let elem: Element = "".parse().unwrap(); #[cfg(feature = "component")] let elem: Element = "" .parse() .unwrap(); let presence = Presence::try_from(elem).unwrap(); assert_eq!(presence.from, None); assert_eq!(presence.to, None); assert_eq!(presence.id, None); assert_eq!(presence.type_, Type::None); assert!(presence.payloads.is_empty()); } // TODO: This test is currently ignored because it serializes // always, so let’s implement that in xso first. The only downside to // having it included is some more bytes on the wire, we can live with that // for now. #[test] #[ignore] fn test_serialise() { #[cfg(not(feature = "component"))] let elem: Element = "" .parse() .unwrap(); #[cfg(feature = "component")] let elem: Element = "" .parse() .unwrap(); let presence = Presence::new(Type::Unavailable); let elem2 = presence.into(); assert_eq!(elem, elem2); } #[test] fn test_show() { #[cfg(not(feature = "component"))] let elem: Element = "chat" .parse() .unwrap(); #[cfg(feature = "component")] let elem: Element = "chat" .parse() .unwrap(); let presence = Presence::try_from(elem).unwrap(); assert_eq!(presence.payloads.len(), 0); assert_eq!(presence.show, Some(Show::Chat)); } #[test] fn test_empty_show_value() { #[cfg(not(feature = "component"))] let elem: Element = "".parse().unwrap(); #[cfg(feature = "component")] let elem: Element = "" .parse() .unwrap(); let presence = Presence::try_from(elem).unwrap(); assert_eq!(presence.show, None); } #[test] fn test_missing_show_value() { #[cfg(not(feature = "component"))] let elem: Element = "" .parse() .unwrap(); #[cfg(feature = "component")] let elem: Element = "" .parse() .unwrap(); let error = Presence::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!(message, "Invalid value for show."); } #[test] fn test_invalid_show() { // "online" used to be a pretty common mistake. #[cfg(not(feature = "component"))] let elem: Element = "online" .parse() .unwrap(); #[cfg(feature = "component")] let elem: Element = "online" .parse() .unwrap(); let error = Presence::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!(message, "Invalid value for show."); } #[test] fn test_empty_status() { #[cfg(not(feature = "component"))] let elem: Element = "" .parse() .unwrap(); #[cfg(feature = "component")] let elem: Element = "" .parse() .unwrap(); let presence = Presence::try_from(elem).unwrap(); assert_eq!(presence.payloads.len(), 0); assert_eq!(presence.statuses.len(), 1); assert_eq!(presence.statuses[""], ""); } #[test] fn test_status() { #[cfg(not(feature = "component"))] let elem: Element = "Here!" .parse() .unwrap(); #[cfg(feature = "component")] let elem: Element = "Here!" .parse() .unwrap(); let presence = Presence::try_from(elem).unwrap(); assert_eq!(presence.payloads.len(), 0); assert_eq!(presence.statuses.len(), 1); assert_eq!(presence.statuses[""], "Here!"); } #[test] fn test_multiple_statuses() { #[cfg(not(feature = "component"))] let elem: Element = "Here!Là!".parse().unwrap(); #[cfg(feature = "component")] let elem: Element = "Here!Là!".parse().unwrap(); let presence = Presence::try_from(elem).unwrap(); assert_eq!(presence.payloads.len(), 0); assert_eq!(presence.statuses.len(), 2); assert_eq!(presence.statuses[""], "Here!"); assert_eq!(presence.statuses["fr"], "Là!"); } // TODO: Enable that test again once xso supports rejecting multiple // identical xml:lang versions. #[test] #[ignore] fn test_invalid_multiple_statuses() { #[cfg(not(feature = "component"))] let elem: Element = "Here!Là!".parse().unwrap(); #[cfg(feature = "component")] let elem: Element = "Here!Là!".parse().unwrap(); let error = Presence::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!( message, "Status element present twice for the same xml:lang." ); } #[test] fn test_priority() { #[cfg(not(feature = "component"))] let elem: Element = "-1" .parse() .unwrap(); #[cfg(feature = "component")] let elem: Element = "-1" .parse() .unwrap(); let presence = Presence::try_from(elem).unwrap(); assert_eq!(presence.payloads.len(), 0); assert_eq!(presence.priority, Priority(-1i8)); } #[test] fn test_invalid_priority() { #[cfg(not(feature = "component"))] let elem: Element = "128" .parse() .unwrap(); #[cfg(feature = "component")] let elem: Element = "128" .parse() .unwrap(); let error = Presence::try_from(elem).unwrap_err(); match error { FromElementError::Invalid(Error::TextParseError(e)) if e.is::() => { () } _ => panic!(), }; } #[test] fn test_unknown_child() { #[cfg(not(feature = "component"))] let elem: Element = "" .parse() .unwrap(); #[cfg(feature = "component")] let elem: Element = "" .parse() .unwrap(); let presence = Presence::try_from(elem).unwrap(); let payload = &presence.payloads[0]; assert!(payload.is("test", "invalid")); } #[cfg(not(feature = "disable-validation"))] #[test] fn test_invalid_status_child() { #[cfg(not(feature = "component"))] let elem: Element = "" .parse() .unwrap(); #[cfg(feature = "component")] let elem: Element = "" .parse() .unwrap(); let error = Presence::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!( message, "Unknown child in extraction for field 'statuses' in Presence element." ); } #[cfg(not(feature = "disable-validation"))] #[test] fn test_invalid_attribute() { #[cfg(not(feature = "component"))] let elem: Element = "" .parse() .unwrap(); #[cfg(feature = "component")] let elem: Element = "" .parse() .unwrap(); let error = Presence::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!( message, "Unknown attribute in extraction for field 'statuses' in Presence element." ); } #[test] fn test_serialise_status() { let status = Status::from("Hello world!"); let mut presence = Presence::new(Type::Unavailable); presence.statuses.insert(Lang::from(""), status); let elem: Element = presence.into(); assert!(elem.is("presence", ns::DEFAULT_NS)); assert!(elem.children().next().unwrap().is("status", ns::DEFAULT_NS)); } #[test] fn test_serialise_priority() { let presence = Presence::new(Type::None).with_priority(42); let elem: Element = presence.into(); assert!(elem.is("presence", ns::DEFAULT_NS)); let priority = elem.children().next().unwrap(); assert!(priority.is("priority", ns::DEFAULT_NS)); assert_eq!(priority.text(), "42"); } #[test] fn presence_with_to() { let presence = Presence::new(Type::None); let elem: Element = presence.into(); assert_eq!(elem.attr(rxml::xml_ncname!("to")), None); let presence = Presence::new(Type::None).with_to(Jid::new("localhost").unwrap()); let elem: Element = presence.into(); assert_eq!(elem.attr(rxml::xml_ncname!("to")), Some("localhost")); let presence = Presence::new(Type::None).with_to(BareJid::new("localhost").unwrap()); let elem: Element = presence.into(); assert_eq!(elem.attr(rxml::xml_ncname!("to")), Some("localhost")); let presence = Presence::new(Type::None).with_to(Jid::new("test@localhost/coucou").unwrap()); let elem: Element = presence.into(); assert_eq!( elem.attr(rxml::xml_ncname!("to")), Some("test@localhost/coucou") ); let presence = Presence::new(Type::None).with_to(FullJid::new("test@localhost/coucou").unwrap()); let elem: Element = presence.into(); assert_eq!( elem.attr(rxml::xml_ncname!("to")), Some("test@localhost/coucou") ); } #[test] fn test_xml_lang() { #[cfg(not(feature = "component"))] let elem: Element = "" .parse() .unwrap(); #[cfg(feature = "component")] let elem: Element = "" .parse() .unwrap(); let presence = Presence::try_from(elem).unwrap(); assert_eq!(presence.from, None); assert_eq!(presence.to, None); assert_eq!(presence.id, None); assert_eq!(presence.type_, Type::None); assert!(presence.payloads.is_empty()); } } xmpp-parsers-0.22.0/src/private.rs000064400000000000000000000043261046102023000151620ustar 00000000000000// Copyright (c) 2023 xmppftw // // 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/. //! //! This module implements [Private XML Storage](https://xmpp.org/extensions/xep-0049.html) from //! XEP-0049. //! //! However, only legacy bookmarks storage from [XEP-0048 //! v1.0](https://xmpp.org/extensions/attic/xep-0048-1.0.html) is supported at the moment. //! This should only be used when `urn:xmpp:bookmarks:1#compat` is not advertised on the user's //! BareJID in a disco info request. //! //! See [ModernXMPP docs](https://docs.modernxmpp.org/client/groupchat/#bookmarks) on how to handle //! all historic and newer specifications for your clients handling of chatroom bookmarks. //! //! This module uses the legacy bookmarks [`bookmarks::Conference`][crate::bookmarks::Conference] //! struct as stored in a legacy [`bookmarks::Storage`][crate::bookmarks::Storage] struct. use xso::{AsXml, FromXml}; use crate::{ bookmarks::Storage, iq::{IqGetPayload, IqResultPayload, IqSetPayload}, ns, }; /// A Private XML Storage query. Only supports XEP-0048 bookmarks. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::PRIVATE, name = "query")] pub struct Query { /// XEP-0048 bookmarks in a [`Storage`] element #[xml(child)] pub storage: Storage, } impl IqSetPayload for Query {} impl IqGetPayload for Query {} impl IqResultPayload for Query {} #[cfg(test)] mod tests { use super::*; use minidom::Element; use xso::error::{Error, FromElementError}; #[test] fn test_invalid_duplicate_child() { let elem: Element = "" .parse() .unwrap(); let error = Query::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!( message, "Query element must not have more than one child in field 'storage'." ); } } xmpp-parsers-0.22.0/src/pubsub/event.rs000064400000000000000000000257341046102023000161370ustar 00000000000000// Copyright (c) 2017 Emmanuel Gil Peyrot // // 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 xso::{AsXml, FromXml}; use crate::data_forms::DataForm; use crate::date::DateTime; use crate::message::MessagePayload; use crate::ns; use crate::pubsub::{ItemId, NodeName, Subscription, SubscriptionId}; use jid::Jid; use minidom::Element; /// An event item from a PubSub node. #[derive(FromXml, AsXml, Debug, Clone, PartialEq)] #[xml(namespace = ns::PUBSUB_EVENT, name = "item")] pub struct Item { /// The identifier for this item, unique per node. #[xml(attribute(default))] pub id: Option, /// The JID of the entity who published this item. #[xml(attribute(default))] pub publisher: Option, /// The payload of this item, in an arbitrary namespace. #[xml(element(default))] pub payload: Option, } /// Represents an event happening to a PubSub node. #[derive(FromXml, AsXml, Debug, Clone, PartialEq)] #[xml(namespace = ns::PUBSUB_EVENT, name = "event")] pub struct Event { /// The inner child of this event. #[xml(child)] pub payload: Payload, } impl MessagePayload for Event {} /// Represents an event happening to a PubSub node. #[derive(FromXml, AsXml, Debug, Clone, PartialEq)] #[xml(namespace = ns::PUBSUB_EVENT, exhaustive)] pub enum Payload { /* Collection { }, */ /// This node’s configuration changed. #[xml(name = "configuration")] Configuration { /// The node affected. #[xml(attribute)] node: NodeName, /// The new configuration of this node. #[xml(child(default))] form: Option, }, /// This node has been deleted, with an optional redirect to another node. #[xml(name = "delete")] Delete { /// The node affected. #[xml(attribute)] node: NodeName, /// The xmpp: URI of another node replacing this one. #[xml(extract(default, fields(attribute(default, name = "uri"))))] redirect: Option, }, /// Some items have been published or retracted on this node. #[xml(name = "items")] Items { /// The node affected. #[xml(attribute)] node: NodeName, /// The list of published items. #[xml(child(n = ..))] published: Vec, /// The list of retracted items. #[xml(extract(n = .., name = "retract", fields(attribute(name = "id", type_ = ItemId))))] retracted: Vec, }, /// All items of this node just got removed at once. #[xml(name = "purge")] Purge { /// The node affected. #[xml(attribute)] node: NodeName, }, /// The user’s subscription to this node has changed. #[xml(name = "subscription")] Subscription { /// The node affected. #[xml(attribute)] node: NodeName, /// The time at which this subscription will expire. #[xml(attribute(default))] expiry: Option, /// The JID of the user affected. #[xml(attribute(default))] jid: Option, /// An identifier for this subscription. #[xml(attribute(default))] subid: Option, /// The state of this subscription. #[xml(attribute(default))] subscription: Option, }, } impl Payload { /// Return the name of the node to which this event is related. pub fn node_name(&self) -> &NodeName { match self { Self::Purge { node, .. } => node, Self::Items { node, .. } => node, Self::Subscription { node, .. } => node, Self::Delete { node, .. } => node, Self::Configuration { node, .. } => node, } } } #[cfg(test)] mod tests { use super::*; use jid::BareJid; use xso::error::{Error, FromElementError}; // TODO: Reenable this test once we support asserting that a Vec isn’t empty. #[test] #[ignore] fn missing_items() { let elem: Element = "" .parse() .unwrap(); let error = Event::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!(message, "Missing children in items element."); } #[test] fn test_simple_items() { let elem: Element = "".parse().unwrap(); let event = Event::try_from(elem).unwrap(); match event.payload { Payload::Items { node, published, retracted, } => { assert_eq!(node, NodeName(String::from("coucou"))); assert_eq!(retracted.len(), 0); assert_eq!(published[0].id, Some(ItemId(String::from("test")))); assert_eq!( published[0].publisher.clone().unwrap(), BareJid::new("test@coucou").unwrap() ); assert_eq!(published[0].payload, None); } _ => panic!(), } } #[test] fn test_simple_pep() { let elem: Element = "".parse().unwrap(); let event = Event::try_from(elem).unwrap(); match event.payload { Payload::Items { node, published, retracted, } => { assert_eq!(node, NodeName(String::from("something"))); assert_eq!(retracted.len(), 0); assert_eq!(published[0].id, None); assert_eq!(published[0].publisher, None); match published[0].payload { Some(ref elem) => assert!(elem.is("foreign", "example:namespace")), _ => panic!(), } } _ => panic!(), } } #[test] fn test_simple_retract() { let elem: Element = "".parse().unwrap(); let event = Event::try_from(elem).unwrap(); match event.payload { Payload::Items { node, published, retracted, } => { assert_eq!(node, NodeName(String::from("something"))); assert_eq!(published.len(), 0); assert_eq!(retracted[0], ItemId(String::from("coucou"))); assert_eq!(retracted[1], ItemId(String::from("test"))); } _ => panic!(), } } #[test] fn test_simple_delete() { let elem: Element = "".parse().unwrap(); let event = Event::try_from(elem).unwrap(); match event.payload { Payload::Delete { node, redirect } => { assert_eq!(node, NodeName(String::from("coucou"))); assert_eq!(redirect, Some(String::from("hello"))); } _ => panic!(), } } #[test] fn test_simple_purge() { let elem: Element = "" .parse() .unwrap(); let event = Event::try_from(elem).unwrap(); match event.payload { Payload::Purge { node } => { assert_eq!(node, NodeName(String::from("coucou"))); } _ => panic!(), } } #[test] fn test_simple_configure() { let elem: Element = "http://jabber.org/protocol/pubsub#node_config".parse().unwrap(); let event = Event::try_from(elem).unwrap(); match event.payload { Payload::Configuration { node, form: _ } => { assert_eq!(node, NodeName(String::from("coucou"))); //assert_eq!(form.type_, Result_); } _ => panic!(), } } #[test] fn test_invalid() { let elem: Element = "" .parse() .unwrap(); let error = Event::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!(message, "This is not a Payload element."); } #[cfg(not(feature = "disable-validation"))] #[test] fn test_invalid_attribute() { let elem: Element = "" .parse() .unwrap(); let error = Event::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!(message, "Unknown attribute in Event element."); } #[test] fn test_ex221_subscription() { let elem: Element = "" .parse() .unwrap(); let event = Event::try_from(elem.clone()).unwrap(); match event.payload.clone() { Payload::Subscription { node, expiry, jid, subid, subscription, } => { assert_eq!(node, NodeName(String::from("princely_musings"))); assert_eq!( subid, Some(SubscriptionId(String::from( "ba49252aaa4f5d320c24d3766f0bdcade78c78d3" ))) ); assert_eq!(subscription, Some(Subscription::Subscribed)); assert_eq!(jid.unwrap(), BareJid::new("francisco@denmark.lit").unwrap()); assert_eq!(expiry, Some("2006-02-28T23:59:59Z".parse().unwrap())); } _ => panic!(), } let elem2: Element = event.into(); assert_eq!(elem, elem2); } } xmpp-parsers-0.22.0/src/pubsub/mod.rs000064400000000000000000000045411046102023000155660ustar 00000000000000// Copyright (c) 2017 Emmanuel Gil Peyrot // // 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/. /// The `http://jabber.org/protocol/pubsub#event` protocol. pub mod event; /// The `http://jabber.org/protocol/pubsub#owner` protocol. pub mod owner; /// The `http://jabber.org/protocol/pubsub` protocol. #[allow(clippy::module_inception)] pub mod pubsub; pub use self::event::Event; pub use self::owner::Owner; pub use self::pubsub::PubSub; use minidom::Element; generate_id!( /// The name of a PubSub node, used to identify it on a JID. NodeName ); generate_id!( /// The identifier of an item, which is unique per node. ItemId ); generate_id!( /// The identifier of a subscription to a PubSub node. SubscriptionId ); generate_attribute!( /// The state of a subscription to a node. Subscription, "subscription", { /// The user is not subscribed to this node. None => "none", /// The user’s subscription to this node is still pending. Pending => "pending", /// The user is subscribed to this node. Subscribed => "subscribed", /// The user’s subscription to this node will only be valid once /// configured. Unconfigured => "unconfigured", }, Default = None ); generate_attribute!( /// A list of possible affiliations to a node. AffiliationAttribute, "affiliation", { /// You are a member of this node, you can subscribe and retrieve items. Member => "member", /// You don’t have a specific affiliation with this node, you can only subscribe to it. None => "none", /// You are banned from this node. Outcast => "outcast", /// You are an owner of this node, and can do anything with it. Owner => "owner", /// You are a publisher on this node, you can publish and retract items to it. Publisher => "publisher", /// You can publish and retract items on this node, but not subscribe or retrieve items. PublishOnly => "publish-only", } ); /// This trait should be implemented on any element which can be included as a PubSub payload. pub trait PubSubPayload: TryFrom + Into {} xmpp-parsers-0.22.0/src/pubsub/owner.rs000064400000000000000000000242071046102023000161420ustar 00000000000000// Copyright (c) 2020 Paul Fariello // Copyright (c) 2018 Emmanuel Gil Peyrot // // 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 xso::{AsXml, FromXml}; use crate::data_forms::DataForm; use crate::iq::{IqGetPayload, IqResultPayload, IqSetPayload}; use crate::ns; use crate::pubsub::{AffiliationAttribute, NodeName, Subscription}; use jid::Jid; /// An affiliation element. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::PUBSUB_OWNER, name = "affiliation")] pub struct Affiliation { /// The node this affiliation pertains to. #[xml(attribute)] jid: Jid, /// The affiliation you currently have on this node. #[xml(attribute)] affiliation: AffiliationAttribute, } /// A subscription element, describing the state of a subscription. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::PUBSUB_OWNER, name = "subscription")] pub struct SubscriptionElem { /// The JID affected by this subscription. #[xml(attribute)] pub jid: Jid, /// The state of the subscription. #[xml(attribute)] pub subscription: Subscription, /// Subscription unique id. #[xml(attribute(default))] pub subid: Option, } /// Represents an owner request to a PubSub service. #[derive(FromXml, AsXml, Debug, Clone, PartialEq)] #[xml(namespace = ns::PUBSUB_OWNER, name = "pubsub")] pub struct Owner { /// The inner child of this request. #[xml(child)] pub payload: Payload, } // TODO: Differentiate each payload per type someday, to simplify our types. impl IqGetPayload for Owner {} impl IqSetPayload for Owner {} impl IqResultPayload for Owner {} /// Main payload used to communicate with a PubSub service. /// /// `` #[derive(FromXml, AsXml, Debug, Clone, PartialEq)] #[xml(namespace = ns::PUBSUB_OWNER, exhaustive)] pub enum Payload { /// Manage the affiliations of a node. #[xml(name = "affiliations")] Affiliations { /// The node name this request pertains to. #[xml(attribute)] node: NodeName, /// The actual list of affiliation elements. #[xml(child(n = ..))] affiliations: Vec, }, /// Request to configure a node. #[xml(name = "configure")] Configure { /// The node to be configured. #[xml(attribute(default))] node: Option, /// The form to configure it. #[xml(child(default))] form: Option, }, /// Request the default node configuration. #[xml(name = "default")] Default { /// The form to configure it. #[xml(child(default))] form: Option, }, /// Delete a node. #[xml(name = "delete")] Delete { /// The node to be deleted. #[xml(attribute)] node: NodeName, /// Redirection to replace the deleted node. #[xml(extract(default, name = "redirect", fields(attribute(name = "uri", type_ = String))))] redirect_uri: Option, }, /// Purge all items from node. #[xml(name = "purge")] Purge { /// The node to be cleared. #[xml(attribute)] node: NodeName, }, /// Request the current subscriptions to a node. #[xml(name = "subscriptions")] Subscriptions { /// The node to query. #[xml(attribute)] node: NodeName, /// The list of subscription elements returned. #[xml(child(n = ..))] subscriptions: Vec, }, } #[cfg(test)] mod tests { use super::*; use crate::data_forms::{DataFormType, Field, FieldType}; use core::str::FromStr; use jid::BareJid; use minidom::Element; #[test] fn affiliations() { let elem: Element = "" .parse() .unwrap(); let elem1 = elem.clone(); let payload = Payload::Affiliations { node: NodeName(String::from("foo")), affiliations: vec![ Affiliation { jid: Jid::from(BareJid::from_str("hamlet@denmark.lit").unwrap()), affiliation: AffiliationAttribute::Owner, }, Affiliation { jid: Jid::from(BareJid::from_str("polonius@denmark.lit").unwrap()), affiliation: AffiliationAttribute::Outcast, }, ], }; let pubsub = Owner { payload }; let elem2 = Element::from(pubsub); assert_eq!(elem1, elem2); } #[test] fn configure() { let elem: Element = "http://jabber.org/protocol/pubsub#node_configwhitelist" .parse() .unwrap(); let elem1 = elem.clone(); let payload = Payload::Configure { node: Some(NodeName(String::from("foo"))), form: Some(DataForm::new( DataFormType::Submit, ns::PUBSUB_CONFIGURE, vec![Field::new("pubsub#access_model", FieldType::ListSingle) .with_value("whitelist")], )), }; let pubsub = Owner { payload }; let elem2 = Element::from(pubsub); assert_eq!(elem1, elem2); } #[test] fn test_serialize_configure() { let reference: Element = "" .parse() .unwrap(); let elem: Element = "".parse().unwrap(); let form = DataForm::try_from(elem).unwrap(); let payload = Payload::Configure { node: Some(NodeName(String::from("foo"))), form: Some(form), }; let configure = Owner { payload }; let serialized: Element = configure.into(); assert_eq!(serialized, reference); } #[test] fn default() { let elem: Element = "http://jabber.org/protocol/pubsub#node_configwhitelist" .parse() .unwrap(); let elem1 = elem.clone(); let payload = Payload::Default { form: Some(DataForm::new( DataFormType::Submit, ns::PUBSUB_CONFIGURE, vec![Field::new("pubsub#access_model", FieldType::ListSingle) .with_value("whitelist")], )), }; let pubsub = Owner { payload }; let elem2 = Element::from(pubsub); assert_eq!(elem1, elem2); } #[test] fn delete() { let elem: Element = "" .parse() .unwrap(); let elem1 = elem.clone(); let payload = Payload::Delete { node: NodeName(String::from("foo")), redirect_uri: Some(String::from("xmpp:hamlet@denmark.lit?;node=blog")), }; let pubsub = Owner { payload }; let elem2 = Element::from(pubsub); assert_eq!(elem1, elem2); } #[test] fn purge() { let elem: Element = "" .parse() .unwrap(); let elem1 = elem.clone(); let payload = Payload::Purge { node: NodeName(String::from("foo")), }; let pubsub = Owner { payload }; let elem2 = Element::from(pubsub); assert_eq!(elem1, elem2); } #[test] fn subscriptions() { let elem: Element = "" .parse() .unwrap(); let elem1 = elem.clone(); let payload = Payload::Subscriptions { node: NodeName(String::from("foo")), subscriptions: vec![ SubscriptionElem { jid: Jid::from(BareJid::from_str("hamlet@denmark.lit").unwrap()), subscription: Subscription::Subscribed, subid: None, }, SubscriptionElem { jid: Jid::from(BareJid::from_str("polonius@denmark.lit").unwrap()), subscription: Subscription::Unconfigured, subid: None, }, SubscriptionElem { jid: Jid::from(BareJid::from_str("bernardo@denmark.lit").unwrap()), subscription: Subscription::Subscribed, subid: Some(String::from("123-abc")), }, SubscriptionElem { jid: Jid::from(BareJid::from_str("bernardo@denmark.lit").unwrap()), subscription: Subscription::Subscribed, subid: Some(String::from("004-yyy")), }, ], }; let pubsub = Owner { payload }; let elem2 = Element::from(pubsub); assert_eq!(elem1, elem2); } } xmpp-parsers-0.22.0/src/pubsub/pubsub.rs000064400000000000000000000646411046102023000163160ustar 00000000000000// Copyright (c) 2018 Emmanuel Gil Peyrot // // 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 xso::{ error::{Error, FromElementError}, AsXml, FromXml, }; use crate::data_forms::DataForm; use crate::iq::{IqGetPayload, IqResultPayload, IqSetPayload}; use crate::ns; use crate::pubsub::{ AffiliationAttribute, ItemId, NodeName, PubSubPayload, Subscription, SubscriptionId, }; use jid::Jid; use minidom::Element; // TODO: a better solution would be to split this into a query and a result elements, like for // XEP-0030. /// A list of affiliations you have on a service, or on a node. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::PUBSUB, name = "affiliations")] pub struct Affiliations { /// The optional node name this request pertains to. #[xml(attribute(default))] pub node: Option, /// The actual list of affiliation elements. #[xml(child(n = ..))] pub affiliations: Vec, } /// An affiliation element. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::PUBSUB, name = "affiliation")] pub struct Affiliation { /// The node this affiliation pertains to. #[xml(attribute)] pub node: NodeName, /// The affiliation you currently have on this node. #[xml(attribute)] pub affiliation: AffiliationAttribute, } /// Request to configure a new node. #[derive(FromXml, AsXml, Debug, PartialEq, Clone)] #[xml(namespace = ns::PUBSUB, name = "configure")] pub struct Configure { /// The form to configure it. #[xml(child(default))] pub form: Option, } /// Request to create a new node. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::PUBSUB, name = "create")] pub struct Create { /// The node name to create, if `None` the service will generate one. #[xml(attribute(default))] pub node: Option, } /// Request for a default node configuration. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::PUBSUB, name = "default")] pub struct Default { /// The node targeted by this request, otherwise the entire service. #[xml(attribute(default))] pub node: Option, // TODO: do we really want to support collection nodes? // #[xml(attribute(default, name = "type"))] // type_: Option, } /// A request for a list of items. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::PUBSUB, name = "items")] pub struct Items { // TODO: should be an xs:positiveInteger, that is, an unbounded int ≥ 1. /// Maximum number of items returned. #[xml(attribute(name = "max_items" /*sic!*/, default))] pub max_items: Option, /// The node queried by this request. #[xml(attribute)] pub node: NodeName, /// The subscription identifier related to this request. #[xml(attribute(default))] pub subid: Option, /// The actual list of items returned. #[xml(child(n = ..))] pub items: Vec, } impl Items { /// Create a new items request. pub fn new(node: &str) -> Items { Items { node: NodeName(String::from(node)), max_items: None, subid: None, items: Vec::new(), } } } /// A requested item from a PubSub node. #[derive(FromXml, AsXml, Debug, Clone, PartialEq)] #[xml(namespace = ns::PUBSUB, name = "item")] pub struct Item { /// The identifier for this item, unique per node. #[xml(attribute(default))] pub id: Option, /// The JID of the entity who published this item. #[xml(attribute(default))] pub publisher: Option, /// The payload of this item, in an arbitrary namespace. #[xml(element(default))] pub payload: Option, } impl Item { /// Create a new item, accepting only payloads implementing `PubSubPayload`. pub fn new( id: Option, publisher: Option, payload: Option

, ) -> Item { Item { id, publisher, payload: payload.map(Into::into), } } } /// The options associated to a subscription request. #[derive(FromXml, AsXml, Debug, PartialEq, Clone)] #[xml(namespace = ns::PUBSUB, name = "options")] pub struct Options { /// The JID affected by this request. #[xml(attribute)] pub jid: Jid, /// The node affected by this request. #[xml(attribute(default))] pub node: Option, /// The subscription identifier affected by this request. #[xml(attribute(default))] pub subid: Option, /// The form describing the subscription. #[xml(child(default))] pub form: Option, } /// Request to publish items to a node. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::PUBSUB, name = "publish")] pub struct Publish { /// The target node for this operation. #[xml(attribute)] pub node: NodeName, /// The items you want to publish. #[xml(child(n = ..))] pub items: Vec, } /// The options associated to a publish request. #[derive(FromXml, AsXml, Debug, PartialEq, Clone)] #[xml(namespace = ns::PUBSUB, name = "publish-options")] pub struct PublishOptions { /// The form describing these options. #[xml(child(default))] pub form: Option, } /// A request to retract some items from a node. #[derive(FromXml, AsXml, Debug, PartialEq, Clone)] #[xml(namespace = ns::PUBSUB, name = "retract")] pub struct Retract { /// The node affected by this request. #[xml(attribute)] pub node: NodeName, /// Whether a retract request should notify subscribers or not. #[xml(attribute(default))] pub notify: bool, /// The items affected by this request. #[xml(child(n = ..))] pub items: Vec, } /// Indicate that the subscription can be configured. #[derive(FromXml, AsXml, Debug, Clone, PartialEq)] #[xml(namespace = ns::PUBSUB, name = "subscribe-options")] pub struct SubscribeOptions { /// If `true`, the configuration is actually required. #[xml(flag)] required: bool, } /// A request to subscribe a JID to a node. #[derive(FromXml, AsXml, Debug, PartialEq, Clone)] #[xml(namespace = ns::PUBSUB, name = "subscribe")] pub struct Subscribe { /// The JID being subscribed. #[xml(attribute)] pub jid: Jid, /// The node to subscribe to. #[xml(attribute)] pub node: Option, } /// A request for current subscriptions. #[derive(FromXml, AsXml, Debug, PartialEq, Clone)] #[xml(namespace = ns::PUBSUB, name = "subscriptions")] pub struct Subscriptions { /// The node to query. #[xml(attribute(default))] pub node: Option, /// The list of subscription elements returned. #[xml(child(n = ..))] pub subscription: Vec, } /// A subscription element, describing the state of a subscription. #[derive(FromXml, AsXml, Debug, PartialEq, Clone)] #[xml(namespace = ns::PUBSUB, name = "subscription")] pub struct SubscriptionElem { /// The JID affected by this subscription. #[xml(attribute)] jid: Jid, /// The node affected by this subscription. #[xml(attribute(default))] node: Option, /// The subscription identifier for this subscription. #[xml(attribute(default))] subid: Option, /// The state of the subscription. #[xml(attribute(default))] subscription: Option, /// The options related to this subscription. #[xml(child(default))] subscribe_options: Option, } /// An unsubscribe request. #[derive(FromXml, AsXml, Debug, PartialEq, Clone)] #[xml(namespace = ns::PUBSUB, name = "unsubscribe")] pub struct Unsubscribe { /// The JID affected by this request. #[xml(attribute)] jid: Jid, /// The node affected by this request. #[xml(attribute)] node: Option, /// The subscription identifier for this subscription. #[xml(attribute)] subid: Option, } /// Main payload used to communicate with a PubSub service. /// /// `` #[derive(Debug, Clone, PartialEq)] pub enum PubSub { /// Request to create a new node, with optional suggested name and suggested configuration. Create { /// The create request. create: Create, /// The configure request for the new node. configure: Option, }, /// A subscribe request. Subscribe { /// The subscribe request. subscribe: Option, /// The options related to this subscribe request. options: Option, }, /// Request to publish items to a node, with optional options. Publish { /// The publish request. publish: Publish, /// The options related to this publish request. publish_options: Option, }, /// A list of affiliations you have on a service, or on a node. Affiliations(Affiliations), /// Request for a default node configuration. Default(Default), /// A request for a list of items. Items(Items), /// A request to retract some items from a node. Retract(Retract), /// A request about a subscription. Subscription(SubscriptionElem), /// A request for current subscriptions. Subscriptions(Subscriptions), /// An unsubscribe request. Unsubscribe(Unsubscribe), } impl IqGetPayload for PubSub {} impl IqSetPayload for PubSub {} impl IqResultPayload for PubSub {} impl TryFrom for PubSub { type Error = FromElementError; fn try_from(elem: Element) -> Result { check_self!(elem, "pubsub", PUBSUB); check_no_attributes!(elem, "pubsub"); let mut payload = None; for child in elem.children() { if child.is("create", ns::PUBSUB) { if payload.is_some() { return Err( Error::Other("Payload is already defined in pubsub element.").into(), ); } let create = Create::try_from(child.clone())?; payload = Some(PubSub::Create { create, configure: None, }); } else if child.is("subscribe", ns::PUBSUB) { if payload.is_some() { return Err( Error::Other("Payload is already defined in pubsub element.").into(), ); } let subscribe = Subscribe::try_from(child.clone())?; payload = Some(PubSub::Subscribe { subscribe: Some(subscribe), options: None, }); } else if child.is("options", ns::PUBSUB) { if let Some(PubSub::Subscribe { subscribe, options }) = payload { if options.is_some() { return Err( Error::Other("Options is already defined in pubsub element.").into(), ); } let options = Some(Options::try_from(child.clone())?); payload = Some(PubSub::Subscribe { subscribe, options }); } else if payload.is_none() { let options = Options::try_from(child.clone())?; payload = Some(PubSub::Subscribe { subscribe: None, options: Some(options), }); } else { return Err( Error::Other("Payload is already defined in pubsub element.").into(), ); } } else if child.is("configure", ns::PUBSUB) { if let Some(PubSub::Create { create, configure }) = payload { if configure.is_some() { return Err(Error::Other( "Configure is already defined in pubsub element.", ) .into()); } let configure = Some(Configure::try_from(child.clone())?); payload = Some(PubSub::Create { create, configure }); } else { return Err( Error::Other("Payload is already defined in pubsub element.").into(), ); } } else if child.is("publish", ns::PUBSUB) { if payload.is_some() { return Err( Error::Other("Payload is already defined in pubsub element.").into(), ); } let publish = Publish::try_from(child.clone())?; payload = Some(PubSub::Publish { publish, publish_options: None, }); } else if child.is("publish-options", ns::PUBSUB) { if let Some(PubSub::Publish { publish, publish_options, }) = payload { if publish_options.is_some() { return Err(Error::Other( "Publish-options are already defined in pubsub element.", ) .into()); } let publish_options = Some(PublishOptions::try_from(child.clone())?); payload = Some(PubSub::Publish { publish, publish_options, }); } else { return Err( Error::Other("Payload is already defined in pubsub element.").into(), ); } } else if child.is("affiliations", ns::PUBSUB) { if payload.is_some() { return Err( Error::Other("Payload is already defined in pubsub element.").into(), ); } let affiliations = Affiliations::try_from(child.clone())?; payload = Some(PubSub::Affiliations(affiliations)); } else if child.is("default", ns::PUBSUB) { if payload.is_some() { return Err( Error::Other("Payload is already defined in pubsub element.").into(), ); } let default = Default::try_from(child.clone())?; payload = Some(PubSub::Default(default)); } else if child.is("items", ns::PUBSUB) { if payload.is_some() { return Err( Error::Other("Payload is already defined in pubsub element.").into(), ); } let items = Items::try_from(child.clone())?; payload = Some(PubSub::Items(items)); } else if child.is("retract", ns::PUBSUB) { if payload.is_some() { return Err( Error::Other("Payload is already defined in pubsub element.").into(), ); } let retract = Retract::try_from(child.clone())?; payload = Some(PubSub::Retract(retract)); } else if child.is("subscription", ns::PUBSUB) { if payload.is_some() { return Err( Error::Other("Payload is already defined in pubsub element.").into(), ); } let subscription = SubscriptionElem::try_from(child.clone())?; payload = Some(PubSub::Subscription(subscription)); } else if child.is("subscriptions", ns::PUBSUB) { if payload.is_some() { return Err( Error::Other("Payload is already defined in pubsub element.").into(), ); } let subscriptions = Subscriptions::try_from(child.clone())?; payload = Some(PubSub::Subscriptions(subscriptions)); } else if child.is("unsubscribe", ns::PUBSUB) { if payload.is_some() { return Err( Error::Other("Payload is already defined in pubsub element.").into(), ); } let unsubscribe = Unsubscribe::try_from(child.clone())?; payload = Some(PubSub::Unsubscribe(unsubscribe)); } else { return Err(Error::Other("Unknown child in pubsub element.").into()); } } payload.ok_or(Error::Other("No payload in pubsub element.").into()) } } impl From for Element { fn from(pubsub: PubSub) -> Element { Element::builder("pubsub", ns::PUBSUB) .append_all(match pubsub { PubSub::Create { create, configure } => { let mut elems = vec![Element::from(create)]; if let Some(configure) = configure { elems.push(Element::from(configure)); } elems } PubSub::Subscribe { subscribe, options } => { let mut elems = vec![]; if let Some(subscribe) = subscribe { elems.push(Element::from(subscribe)); } if let Some(options) = options { elems.push(Element::from(options)); } elems } PubSub::Publish { publish, publish_options, } => { let mut elems = vec![Element::from(publish)]; if let Some(publish_options) = publish_options { elems.push(Element::from(publish_options)); } elems } PubSub::Affiliations(affiliations) => vec![Element::from(affiliations)], PubSub::Default(default) => vec![Element::from(default)], PubSub::Items(items) => vec![Element::from(items)], PubSub::Retract(retract) => vec![Element::from(retract)], PubSub::Subscription(subscription) => vec![Element::from(subscription)], PubSub::Subscriptions(subscriptions) => vec![Element::from(subscriptions)], PubSub::Unsubscribe(unsubscribe) => vec![Element::from(unsubscribe)], }) .build() } } #[cfg(test)] mod tests { use super::*; use crate::data_forms::{DataFormType, Field, FieldType}; #[test] fn create() { let elem: Element = "" .parse() .unwrap(); let elem1 = elem.clone(); let pubsub = PubSub::try_from(elem).unwrap(); match pubsub.clone() { PubSub::Create { create, configure } => { assert!(create.node.is_none()); assert!(configure.is_none()); } _ => panic!(), } let elem2 = Element::from(pubsub); assert_eq!(elem1, elem2); let elem: Element = "" .parse() .unwrap(); let elem1 = elem.clone(); let pubsub = PubSub::try_from(elem).unwrap(); match pubsub.clone() { PubSub::Create { create, configure } => { assert_eq!(&create.node.unwrap().0, "coucou"); assert!(configure.is_none()); } _ => panic!(), } let elem2 = Element::from(pubsub); assert_eq!(elem1, elem2); } #[test] fn create_and_configure_empty() { let elem: Element = "" .parse() .unwrap(); let elem1 = elem.clone(); let pubsub = PubSub::try_from(elem).unwrap(); match pubsub.clone() { PubSub::Create { create, configure } => { assert!(create.node.is_none()); assert!(configure.unwrap().form.is_none()); } _ => panic!(), } let elem2 = Element::from(pubsub); assert_eq!(elem1, elem2); } #[test] fn create_and_configure_simple() { // XXX: Do we want xmpp-parsers to always specify the field type in the output Element? let elem: Element = "http://jabber.org/protocol/pubsub#node_configwhitelist" .parse() .unwrap(); let elem1 = elem.clone(); let pubsub = PubSub::Create { create: Create { node: Some(NodeName(String::from("foo"))), }, configure: Some(Configure { form: Some(DataForm::new( DataFormType::Submit, ns::PUBSUB_CONFIGURE, vec![Field::new("pubsub#access_model", FieldType::ListSingle) .with_value("whitelist")], )), }), }; let elem2 = Element::from(pubsub); assert_eq!(elem1, elem2); } #[test] fn publish() { let elem: Element = "" .parse() .unwrap(); let elem1 = elem.clone(); let pubsub = PubSub::try_from(elem).unwrap(); match pubsub.clone() { PubSub::Publish { publish, publish_options, } => { assert_eq!(&publish.node.0, "coucou"); assert!(publish_options.is_none()); } _ => panic!(), } let elem2 = Element::from(pubsub); assert_eq!(elem1, elem2); } #[test] fn publish_with_publish_options() { let elem: Element = "".parse().unwrap(); let elem1 = elem.clone(); let pubsub = PubSub::try_from(elem).unwrap(); match pubsub.clone() { PubSub::Publish { publish, publish_options, } => { assert_eq!(&publish.node.0, "coucou"); assert!(publish_options.unwrap().form.is_none()); } _ => panic!(), } let elem2 = Element::from(pubsub); assert_eq!(elem1, elem2); } #[test] fn invalid_empty_pubsub() { let elem: Element = "" .parse() .unwrap(); let error = PubSub::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!(message, "No payload in pubsub element."); } #[test] fn publish_option() { let elem: Element = "http://jabber.org/protocol/pubsub#publish-options".parse().unwrap(); let publish_options = PublishOptions::try_from(elem).unwrap(); assert_eq!( publish_options.form.unwrap().form_type().unwrap(), "http://jabber.org/protocol/pubsub#publish-options" ); } #[test] fn subscribe_options() { let elem1: Element = "" .parse() .unwrap(); let subscribe_options1 = SubscribeOptions::try_from(elem1).unwrap(); assert_eq!(subscribe_options1.required, false); let elem2: Element = "".parse().unwrap(); let subscribe_options2 = SubscribeOptions::try_from(elem2).unwrap(); assert_eq!(subscribe_options2.required, true); } #[test] fn test_options_without_subscribe() { let elem: Element = "".parse().unwrap(); let elem1 = elem.clone(); let pubsub = PubSub::try_from(elem).unwrap(); match pubsub.clone() { PubSub::Subscribe { subscribe, options } => { assert!(subscribe.is_none()); assert!(options.is_some()); } _ => panic!(), } let elem2 = Element::from(pubsub); assert_eq!(elem1, elem2); } #[test] fn test_serialize_options() { let reference: Element = "" .parse() .unwrap(); let elem: Element = "".parse().unwrap(); let form = DataForm::try_from(elem).unwrap(); let options = Options { jid: Jid::new("juliet@capulet.lit/balcony").unwrap(), node: None, subid: None, form: Some(form), }; let serialized: Element = options.into(); assert_eq!(serialized, reference); } #[test] fn test_serialize_publish_options() { let reference: Element = "" .parse() .unwrap(); let elem: Element = "".parse().unwrap(); let form = DataForm::try_from(elem).unwrap(); let options = PublishOptions { form: Some(form) }; let serialized: Element = options.into(); assert_eq!(serialized, reference); } } xmpp-parsers-0.22.0/src/push.rs000064400000000000000000000137471046102023000144760ustar 00000000000000// Copyright (c) 2025 saarko // // 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 crate::data_forms::DataForm; use crate::iq::IqSetPayload; use crate::jid::BareJid; use crate::ns; use crate::pubsub::PubSubPayload; use minidom::Element; use xso::{AsXml, FromXml}; /// An enable element for push notifications #[derive(Debug, Clone, FromXml, AsXml)] #[xml(namespace = ns::PUSH, name = "enable")] pub struct Enable { /// The 'jid' attribute of the XMPP Push Service being enabled. #[xml(attribute)] pub jid: BareJid, /// The 'node' attribute which is set to the provisioned node specified by the App Server. #[xml(attribute(default))] pub node: Option, /// Optional additional information to be provided with each published notification, such as authentication credentials. #[xml(child(default))] pub form: Option, } impl IqSetPayload for Enable {} /// A disable element for push notifications #[derive(Debug, Clone, FromXml, AsXml)] #[xml(namespace = ns::PUSH, name = "disable")] pub struct Disable { /// The 'jid' attribute of the XMPP Push Service being disabled. #[xml(attribute)] pub jid: BareJid, /// The 'node' attribute which was set to the provisioned node specified by the App Server. #[xml(attribute(default))] pub node: Option, } impl IqSetPayload for Disable {} /// A notification element containing push notification data #[derive(Debug, Clone, FromXml, AsXml)] #[xml(namespace = ns::PUSH, name = "notification")] pub struct Notification { /// The 'form' to provide summarized information such as the number of unread messages or number of pending subscription requests. #[xml(child(default))] pub form: Option, /// Child elements for the notification that are not part of the summary data form. #[xml(element(n = ..))] pub payloads: Vec, } impl PubSubPayload for Notification {} #[cfg(test)] mod tests { use super::*; use std::str::FromStr; #[test] fn test_enable() { let test_enable = r#" http://jabber.org/protocol/pubsub#publish-options eruio234vzxc2kla-91 "#; let elem = Element::from_str(test_enable).expect("Failed to parse XML"); let enable = Enable::try_from(elem).expect("Failed to parse enable"); assert_eq!( enable.jid, BareJid::from_str("push-5.client.example").unwrap() ); assert_eq!(enable.node.unwrap(), "yxs32uqsflafdk3iuqo"); assert!(enable.form.is_some()); } #[test] fn test_enable_only_with_required_fields() { let test_enable = r#""#; let elem = Element::from_str(test_enable).expect("Failed to parse XML"); let enable = Enable::try_from(elem).expect("Failed to parse enable"); assert_eq!( enable.jid, BareJid::from_str("push-5.client.example").unwrap() ); assert!(enable.node.is_none()); assert!(enable.form.is_none()); } #[test] fn test_disable() { let test_disable = r#""#; let elem = Element::from_str(test_disable).expect("Failed to parse XML"); let disable = Disable::try_from(elem).expect("Failed to parse disable"); assert_eq!( disable.jid, BareJid::from_str("push-5.client.example").unwrap() ); assert_eq!(disable.node.unwrap(), "yxs32uqsflafdk3iuqo"); } #[test] fn test_disable_only_with_required_fields() { let test_disable = r#""#; let elem = Element::from_str(test_disable).expect("Failed to parse XML"); let disable = Disable::try_from(elem).expect("Failed to parse disable"); assert_eq!( disable.jid, BareJid::from_str("push-5.client.example").unwrap() ); assert!(disable.node.is_none()); } #[test] fn test_notification() { let test_notification = r#" urn:xmpp:push:summary 1 juliet@capulet.example/balcony Wherefore art thou, Romeo? Additional custom elements "#; let elem = Element::from_str(test_notification).expect("Failed to parse XML"); let notification = Notification::try_from(elem).expect("Failed to parse notification"); assert!(notification.form.is_some()); assert_eq!(notification.payloads.len(), 1); } #[test] fn test_notification_only_with_required_fields() { let test_notification = r#" Additional custom elements "#; let elem = Element::from_str(test_notification).expect("Failed to parse XML"); let notification = Notification::try_from(elem).expect("Failed to parse notification"); assert!(notification.form.is_none()); assert_eq!(notification.payloads.len(), 1); } } xmpp-parsers-0.22.0/src/reactions.rs000064400000000000000000000056041046102023000154770ustar 00000000000000// Copyright (c) 2022 Emmanuel Gil Peyrot // // 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 xso::{AsXml, FromXml}; use crate::message::MessagePayload; use crate::ns; /// Container for a set of reactions. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::REACTIONS, name = "reactions")] pub struct Reactions { /// The id of the message these reactions apply to. #[xml(attribute)] pub id: String, /// The list of reactions. #[xml(child(n = ..))] pub reactions: Vec, } impl MessagePayload for Reactions {} /// One emoji reaction. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::REACTIONS, name = "reaction")] pub struct Reaction { /// The text of this reaction. #[xml(text)] pub emoji: String, } #[cfg(test)] mod tests { use super::*; use minidom::Element; #[cfg(target_pointer_width = "32")] #[test] fn test_size() { assert_size!(Reactions, 24); assert_size!(Reaction, 12); } #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(Reactions, 48); assert_size!(Reaction, 24); } #[test] fn test_empty() { let elem: Element = "" .parse() .unwrap(); let elem2 = elem.clone(); let reactions = Reactions::try_from(elem2).unwrap(); assert_eq!(reactions.id, "foo"); assert_eq!(reactions.reactions.len(), 0); } #[test] fn test_multi() { let elem: Element = "👋🐢" .parse() .unwrap(); let elem2 = elem.clone(); let reactions = Reactions::try_from(elem2).unwrap(); assert_eq!(reactions.id, "foo"); assert_eq!(reactions.reactions.len(), 2); let [hand, turtle]: [Reaction; 2] = reactions.reactions.try_into().unwrap(); assert_eq!(hand.emoji, "👋"); assert_eq!(turtle.emoji, "🐢"); } #[test] fn test_zwj_emoji() { let elem: Element = "👩🏾‍❤️‍👩🏼" .parse() .unwrap(); let elem2 = elem.clone(); let mut reactions = Reactions::try_from(elem2).unwrap(); assert_eq!(reactions.id, "foo"); assert_eq!(reactions.reactions.len(), 1); let reaction = reactions.reactions.pop().unwrap(); assert_eq!( reaction.emoji, "\u{1F469}\u{1F3FE}\u{200D}\u{2764}\u{FE0F}\u{200D}\u{1F469}\u{1F3FC}" ); } } xmpp-parsers-0.22.0/src/receipts.rs000064400000000000000000000052201046102023000153200ustar 00000000000000// Copyright (c) 2017 Emmanuel Gil Peyrot // // 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 xso::{AsXml, FromXml}; use crate::message::MessagePayload; use crate::ns; /// Requests that this message is acked by the final recipient once /// received. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::RECEIPTS, name = "request")] pub struct Request; impl MessagePayload for Request {} /// Notes that a previous message has correctly been received, it is /// referenced by its 'id' attribute. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::RECEIPTS, name = "received")] pub struct Received { /// The 'id' attribute of the received message. #[xml(attribute)] pub id: String, } impl MessagePayload for Received {} #[cfg(test)] mod tests { use super::*; use crate::ns; use minidom::Element; use xso::error::{Error, FromElementError}; use xso::exports::rxml; #[cfg(target_pointer_width = "32")] #[test] fn test_size() { assert_size!(Request, 0); assert_size!(Received, 12); } #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(Request, 0); assert_size!(Received, 24); } #[test] fn test_simple() { let elem: Element = "".parse().unwrap(); Request::try_from(elem).unwrap(); let elem: Element = "" .parse() .unwrap(); Received::try_from(elem).unwrap(); } #[test] fn test_missing_id() { let elem: Element = "".parse().unwrap(); let error = Received::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!( message, "Required attribute field 'id' on Received element missing." ); } #[test] fn test_serialise() { let receipt = Request; let elem: Element = receipt.into(); assert!(elem.is("request", ns::RECEIPTS)); assert_eq!(elem.attrs().into_iter().count(), 0); let receipt = Received { id: String::from("coucou"), }; let elem: Element = receipt.into(); assert!(elem.is("received", ns::RECEIPTS)); assert_eq!(elem.attr(rxml::xml_ncname!("id")), Some("coucou")); } } xmpp-parsers-0.22.0/src/roster.rs000064400000000000000000000247051046102023000150310ustar 00000000000000// Copyright (c) 2017 Emmanuel Gil Peyrot // // 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 xso::{AsXml, FromXml}; use jid::BareJid; use crate::iq::{IqGetPayload, IqResultPayload, IqSetPayload}; use crate::ns; generate_elem_id!( /// Represents a group a contact is part of. Group, "group", ROSTER ); generate_attribute!( /// The state of your mutual subscription with a contact. Subscription, "subscription", { /// The user doesn’t have any subscription to this contact’s presence, /// and neither does this contact. None => "none", /// Only this contact has a subscription with you, not the opposite. From => "from", /// Only you have a subscription with this contact, not the opposite. To => "to", /// Both you and your contact are subscribed to each other’s presence. Both => "both", /// In a roster set, this asks the server to remove this contact item /// from your roster. Remove => "remove", }, Default = None ); generate_attribute!( /// The sub-state of subscription with a contact. Ask, "ask", ( /// Pending sub-state of the 'none' subscription state. Subscribe => "subscribe" ) ); /// Contact from the user’s contact list. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::ROSTER, name = "item")] pub struct Item { /// JID of this contact. #[xml(attribute)] pub jid: BareJid, /// Name of this contact. #[xml(attribute(default))] pub name: Option, /// Subscription status of this contact. #[xml(attribute(default))] pub subscription: Subscription, /// Indicates “Pending Out” sub-states for this contact. #[xml(attribute(default))] pub ask: Ask, /// Groups this contact is part of. #[xml(child(n = ..))] pub groups: Vec, } /// The contact list of the user. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::ROSTER, name = "query")] pub struct Roster { /// Version of the contact list. /// /// This is an opaque string that should only be sent back to the server on /// a new connection, if this client is storing the contact list between /// connections. #[xml(attribute(default))] pub ver: Option, /// List of the contacts of the user. #[xml(child(n = ..))] pub items: Vec, } impl IqGetPayload for Roster {} impl IqSetPayload for Roster {} impl IqResultPayload for Roster {} #[cfg(test)] mod tests { use super::*; use core::str::FromStr; use minidom::Element; use xso::error::{Error, FromElementError}; #[cfg(target_pointer_width = "32")] #[test] fn test_size() { assert_size!(Group, 12); assert_size!(Subscription, 1); assert_size!(Ask, 1); assert_size!(Item, 44); assert_size!(Roster, 24); } #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(Group, 24); assert_size!(Subscription, 1); assert_size!(Ask, 1); assert_size!(Item, 88); assert_size!(Roster, 48); } #[test] fn test_get() { let elem: Element = "".parse().unwrap(); let roster = Roster::try_from(elem).unwrap(); assert!(roster.ver.is_none()); assert!(roster.items.is_empty()); } #[test] fn test_result() { let elem: Element = "".parse().unwrap(); let roster = Roster::try_from(elem).unwrap(); assert_eq!(roster.ver, Some(String::from("ver7"))); assert_eq!(roster.items.len(), 2); let elem: Element = "" .parse() .unwrap(); let roster = Roster::try_from(elem).unwrap(); assert_eq!(roster.ver, Some(String::from("ver9"))); assert!(roster.items.is_empty()); let elem: Element = r#" Friends MyBuddies "# .parse() .unwrap(); let roster = Roster::try_from(elem).unwrap(); assert_eq!(roster.ver, Some(String::from("ver11"))); assert_eq!(roster.items.len(), 4); assert_eq!( roster.items[0].jid, BareJid::new("romeo@example.net").unwrap() ); assert_eq!(roster.items[0].name, Some(String::from("Romeo"))); assert_eq!(roster.items[0].subscription, Subscription::Both); assert_eq!(roster.items[0].ask, Ask::None); assert_eq!( roster.items[0].groups, vec!(Group::from_str("Friends").unwrap()) ); assert_eq!( roster.items[3].jid, BareJid::new("contact@example.org").unwrap() ); assert_eq!(roster.items[3].name, Some(String::from("MyContact"))); assert_eq!(roster.items[3].subscription, Subscription::None); assert_eq!(roster.items[3].ask, Ask::Subscribe); assert_eq!( roster.items[3].groups, vec!(Group::from_str("MyBuddies").unwrap()) ); } #[test] fn test_multiple_groups() { let elem: Element = "AB" .parse() .unwrap(); let elem1 = elem.clone(); let roster = Roster::try_from(elem).unwrap(); assert!(roster.ver.is_none()); assert_eq!(roster.items.len(), 1); assert_eq!( roster.items[0].jid, BareJid::new("test@example.org").unwrap() ); assert_eq!(roster.items[0].name, None); assert_eq!(roster.items[0].groups.len(), 2); assert_eq!(roster.items[0].groups[0], Group::from_str("A").unwrap()); assert_eq!(roster.items[0].groups[1], Group::from_str("B").unwrap()); let elem2 = roster.into(); assert_eq!(elem1, elem2); } #[test] fn test_set() { let elem: Element = "" .parse() .unwrap(); let roster = Roster::try_from(elem).unwrap(); assert!(roster.ver.is_none()); assert_eq!(roster.items.len(), 1); let elem: Element = r#" Servants "# .parse() .unwrap(); let roster = Roster::try_from(elem).unwrap(); assert!(roster.ver.is_none()); assert_eq!(roster.items.len(), 1); assert_eq!( roster.items[0].jid, BareJid::new("nurse@example.com").unwrap() ); assert_eq!(roster.items[0].name, Some(String::from("Nurse"))); assert_eq!(roster.items[0].groups.len(), 1); assert_eq!( roster.items[0].groups[0], Group::from_str("Servants").unwrap() ); let elem: Element = r#" "# .parse() .unwrap(); let roster = Roster::try_from(elem).unwrap(); assert!(roster.ver.is_none()); assert_eq!(roster.items.len(), 1); assert_eq!( roster.items[0].jid, BareJid::new("nurse@example.com").unwrap() ); assert!(roster.items[0].name.is_none()); assert!(roster.items[0].groups.is_empty()); assert_eq!(roster.items[0].subscription, Subscription::Remove); } #[cfg(not(feature = "disable-validation"))] #[test] fn test_invalid() { let elem: Element = "" .parse() .unwrap(); let error = Roster::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!(message, "Unknown child in Roster element."); let elem: Element = "" .parse() .unwrap(); let error = Roster::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!(message, "Unknown attribute in Roster element."); } #[test] fn test_item_missing_jid() { let elem: Element = "" .parse() .unwrap(); let error = Roster::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!( message, "Required attribute field 'jid' on Item element missing." ); } #[test] fn test_item_invalid_jid() { let elem: Element = "" .parse() .unwrap(); let error = Roster::try_from(elem).unwrap_err(); assert_eq!( format!("{error}"), "text parse error: domain doesn’t pass idna validation" ); } #[test] #[cfg_attr(feature = "disable-validation", should_panic = "Result::unwrap_err")] fn test_item_unknown_child() { let elem: Element = "" .parse() .unwrap(); let error = Roster::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!(message, "Unknown child in Item element."); } } xmpp-parsers-0.22.0/src/rsm.rs000064400000000000000000000154701046102023000143130ustar 00000000000000// Copyright (c) 2017 Emmanuel Gil Peyrot // // 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 xso::{AsXml, FromXml}; use crate::ns; /// Requests paging through a potentially big set of items (represented by an /// UID). #[derive(FromXml, AsXml, Debug, Clone, PartialEq)] #[xml(namespace = ns::RSM, name = "set")] pub struct SetQuery { /// Limit the number of items, or use the recipient’s defaults if None. #[xml(extract(default, fields(text(type_ = usize))))] pub max: Option, /// The UID after which to give results, or if None it is the element /// “before” the first item, effectively an index of negative one. #[xml(extract(default, fields(text(type_ = String))))] pub after: Option, /// The UID before which to give results, or if None it starts with the /// last page of the full set. #[xml(extract(default, fields(text(type_ = String))))] pub before: Option, /// Numerical index of the page (deprecated). #[xml(extract(default, fields(text(type_ = usize))))] pub index: Option, } /// The first item of the page. #[derive(FromXml, AsXml, Debug, Clone, PartialEq)] #[xml(namespace = ns::RSM, name = "first")] pub struct First { /// The position of the [first item](#structfield.item) in the full set /// (which may be approximate). #[xml(attribute(default))] pub index: Option, /// The UID of the first item of the page. #[xml(text)] pub item: String, } /// Describes the paging result of a [query](struct.SetQuery.html). #[derive(FromXml, AsXml, Debug, Clone, PartialEq)] #[xml(namespace = ns::RSM, name = "set")] pub struct SetResult { /// The first item of the page. #[xml(child(default))] pub first: Option, /// The UID of the last item of the page. #[xml(extract(default, fields(text(type_ = String))))] pub last: Option, /// How many items there are in the full set (which may be approximate). #[xml(extract(default, fields(text(type_ = usize))))] pub count: Option, } #[cfg(test)] mod tests { use super::*; use minidom::Element; use xso::error::{Error, FromElementError}; #[cfg(target_pointer_width = "32")] #[test] fn test_size() { assert_size!(SetQuery, 40); assert_size!(SetResult, 40); } #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(SetQuery, 80); assert_size!(SetResult, 80); } #[test] fn test_simple() { let elem: Element = "" .parse() .unwrap(); let set = SetQuery::try_from(elem).unwrap(); assert_eq!(set.max, None); assert_eq!(set.after, None); assert_eq!(set.before, None); assert_eq!(set.index, None); let elem: Element = "" .parse() .unwrap(); let set = SetResult::try_from(elem).unwrap(); match set.first { Some(_) => panic!(), None => (), } assert_eq!(set.last, None); assert_eq!(set.count, None); } #[test] fn test_unknown() { let elem: Element = "" .parse() .unwrap(); let error = SetQuery::try_from(elem.clone()).unwrap_err(); let returned_elem = match error { FromElementError::Mismatch(elem) => elem, _ => panic!(), }; assert_eq!(elem, returned_elem); let elem: Element = "" .parse() .unwrap(); let error = SetResult::try_from(elem.clone()).unwrap_err(); let returned_elem = match error { FromElementError::Mismatch(elem) => elem, _ => panic!(), }; assert_eq!(elem, returned_elem); } #[test] fn test_invalid_child() { let elem: Element = "" .parse() .unwrap(); let error = SetQuery::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!(message, "Unknown child in SetQuery element."); let elem: Element = "" .parse() .unwrap(); let error = SetResult::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!(message, "Unknown child in SetResult element."); } #[test] fn test_serialise() { let elem: Element = "" .parse() .unwrap(); let rsm = SetQuery { max: None, after: None, before: None, index: None, }; let elem2 = rsm.into(); assert_eq!(elem, elem2); let elem: Element = "" .parse() .unwrap(); let rsm = SetResult { first: None, last: None, count: None, }; let elem2 = rsm.into(); assert_eq!(elem, elem2); } // TODO: This test is only ignored because and aren’t equal in // minidom, let’s fix that instead! #[test] #[ignore] fn test_serialise_empty_before() { let elem: Element = "" .parse() .unwrap(); let rsm = SetQuery { max: None, after: None, before: Some("".into()), index: None, }; let elem2 = rsm.into(); assert_eq!(elem, elem2); } #[test] fn test_first_index() { let elem: Element = "coucou" .parse() .unwrap(); let elem1 = elem.clone(); let set = SetResult::try_from(elem).unwrap(); let first = set.first.unwrap(); assert_eq!(first.item, "coucou"); assert_eq!(first.index, Some(4)); let set2 = SetResult { first: Some(First { item: String::from("coucou"), index: Some(4), }), last: None, count: None, }; let elem2 = set2.into(); assert_eq!(elem1, elem2); } } xmpp-parsers-0.22.0/src/rtt.rs000064400000000000000000000123231046102023000143150ustar 00000000000000// Copyright (c) 2022 Emmanuel Gil Peyrot // // 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 xso::{text::EmptyAsNone, AsXml, FromXml}; use crate::ns; generate_attribute!( /// Events for real-time text. Event, "event", { /// Begin a new real-time message. New => "new", /// Re-initialize the real-time message. Reset => "reset", /// Modify existing real-time message. Edit => "edit", /// Signals activation of real-time text. Init => "init", /// Signals deactivation of real-time text. Cancel => "cancel", }, Default = Edit ); generate_attribute!( /// The number of codepoints to erase. Num, "n", u32, Default = 1 ); /// Choice between the three possible actions. #[derive(FromXml, AsXml, Debug, Clone, PartialEq)] #[xml(namespace = ns::RTT, exhaustive)] pub enum Action { /// Supports the transmission of text, including key presses, and text block inserts. #[xml(name = "t")] Insert { /// Position in the message to start inserting from. If None, this means to start from the /// end of the message. #[xml(attribute(default, name = "p"))] pos: Option, /// Text to insert. #[xml(text = EmptyAsNone)] text: Option, }, /// Supports the behavior of backspace key presses. Text is removed towards beginning of the /// message. This element is also used for all delete operations, including the backspace key, /// the delete key, and text block deletes. #[xml(name = "e")] Erase { /// Position in the message to start erasing from. If None, this means to start from the end /// of the message. #[xml(attribute(default))] pos: Option, /// Amount of characters to erase, to the left. #[xml(attribute(default))] num: Num, }, /// Allow for the transmission of intervals, between real-time text actions, to recreate the /// pauses between key presses. #[xml(name = "w")] Wait { /// Amount of milliseconds to wait before the next action. #[xml(attribute = "n")] time: u32, }, } /// Element transmitted at regular interval by the sender client while a message is being composed. #[derive(FromXml, AsXml, Debug, Clone, PartialEq)] #[xml(namespace = ns::RTT, name = "rtt")] pub struct Rtt { /// Counter to maintain synchronisation of real-time text. Senders MUST increment this value /// by 1 for each subsequent edit to the same real-time message, including when appending new /// text. Receiving clients MUST monitor this 'seq' value as a lightweight verification on the /// synchronization of real-time text messages. The bounds of 'seq' is 31-bits, the range of /// positive values for a signed 32-bit integer. #[xml(attribute)] pub seq: u32, /// This attribute signals events for real-time text. #[xml(attribute(default))] pub event: Event, /// When editing a message using XEP-0308, this references the id of the message being edited. #[xml(attribute(default))] pub id: Option, /// Vector of actions being transmitted by this element. #[xml(child(n = ..))] pub actions: Vec, } #[cfg(test)] mod tests { use super::*; use minidom::Element; #[cfg(target_pointer_width = "32")] #[test] fn test_size() { assert_size!(Event, 1); assert_size!(Action, 20); assert_size!(Rtt, 32); } #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(Event, 1); assert_size!(Action, 32); assert_size!(Rtt, 56); } #[test] fn simple() { let elem: Element = "".parse().unwrap(); let rtt = Rtt::try_from(elem).unwrap(); assert_eq!(rtt.seq, 0); assert_eq!(rtt.event, Event::Edit); assert_eq!(rtt.id, None); assert_eq!(rtt.actions.len(), 0); } #[test] fn sequence() { let elem: Element = "Hello,!" .parse() .unwrap(); let rtt = Rtt::try_from(elem).unwrap(); assert_eq!(rtt.seq, 0); assert_eq!(rtt.event, Event::New); assert_eq!(rtt.id, None); let mut actions = rtt.actions.into_iter(); assert_eq!(actions.len(), 4); let Action::Insert { pos, text } = actions.next().unwrap() else { panic!() }; assert_eq!(pos, None); assert_eq!(text.unwrap(), "Hello,"); let Action::Wait { time } = actions.next().unwrap() else { panic!() }; assert_eq!(time, 50); let Action::Erase { pos, num } = actions.next().unwrap() else { panic!() }; assert_eq!(pos, None); assert_eq!(num, Num(1)); let Action::Insert { pos, text } = actions.next().unwrap() else { panic!() }; assert_eq!(pos, None); assert_eq!(text.unwrap(), "!"); } } xmpp-parsers-0.22.0/src/sasl.rs000064400000000000000000000212541046102023000144510ustar 00000000000000// Copyright (c) 2018 Emmanuel Gil Peyrot // // 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 xso::{text::Base64, AsXml, FromXml}; use crate::ns; use alloc::collections::BTreeMap; generate_attribute!( /// The list of available SASL mechanisms. Mechanism, "mechanism", { /// Uses no hashing mechanism and transmit the password in clear to the /// server, using a single step. Plain => "PLAIN", /// Challenge-based mechanism using HMAC and SHA-1, allows both the /// client and the server to avoid having to store the password in /// clear. /// /// See ScramSha1 => "SCRAM-SHA-1", /// Same as [ScramSha1](#structfield.ScramSha1), with the addition of /// channel binding. ScramSha1Plus => "SCRAM-SHA-1-PLUS", /// Same as [ScramSha1](#structfield.ScramSha1), but using SHA-256 /// instead of SHA-1 as the hash function. ScramSha256 => "SCRAM-SHA-256", /// Same as [ScramSha256](#structfield.ScramSha256), with the addition /// of channel binding. ScramSha256Plus => "SCRAM-SHA-256-PLUS", /// Creates a temporary JID on login, which will be destroyed on /// disconnect. Anonymous => "ANONYMOUS", } ); /// The first step of the SASL process, selecting the mechanism and sending /// the first part of the handshake. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::SASL, name = "auth")] pub struct Auth { /// The mechanism used. #[xml(attribute)] pub mechanism: Mechanism, /// The content of the handshake. #[xml(text = Base64)] pub data: Vec, } /// In case the mechanism selected at the [auth](struct.Auth.html) step /// requires a second step, the server sends this element with additional /// data. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::SASL, name = "challenge")] pub struct Challenge { /// The challenge data. #[xml(text = Base64)] pub data: Vec, } /// In case the mechanism selected at the [auth](struct.Auth.html) step /// requires a second step, this contains the client’s response to the /// server’s [challenge](struct.Challenge.html). #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::SASL, name = "response")] pub struct Response { /// The response data. #[xml(text = Base64)] pub data: Vec, } /// Sent by the client at any point after [auth](struct.Auth.html) if it /// wants to cancel the current authentication process. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::SASL, name = "abort")] pub struct Abort; /// Sent by the server on SASL success. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::SASL, name = "success")] pub struct Success { /// Possible data sent on success. #[xml(text = Base64)] pub data: Vec, } /// List of possible failure conditions for SASL. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::SASL)] pub enum DefinedCondition { /// The client aborted the authentication with /// [abort](struct.Abort.html). #[xml(name = "aborted")] Aborted, /// The account the client is trying to authenticate against has been /// disabled. #[xml(name = "account-disabled")] AccountDisabled, /// The credentials for this account have expired. #[xml(name = "credentials-expired")] CredentialsExpired, /// You must enable StartTLS or use direct TLS before using this /// authentication mechanism. #[xml(name = "encryption-required")] EncryptionRequired, /// The base64 data sent by the client is invalid. #[xml(name = "incorrect-encoding")] IncorrectEncoding, /// The authzid provided by the client is invalid. #[xml(name = "invalid-authzid")] InvalidAuthzid, /// The client tried to use an invalid mechanism, or none. #[xml(name = "invalid-mechanism")] InvalidMechanism, /// The client sent a bad request. #[xml(name = "malformed-request")] MalformedRequest, /// The mechanism selected is weaker than what the server allows. #[xml(name = "mechanism-too-weak")] MechanismTooWeak, /// The credentials provided are invalid. #[xml(name = "not-authorized")] NotAuthorized, /// The server encountered an issue which may be fixed later, the /// client should retry at some point. #[xml(name = "temporary-auth-failure")] TemporaryAuthFailure, } type Lang = String; /// Sent by the server on SASL failure. #[derive(FromXml, AsXml, Debug, Clone)] #[xml(namespace = ns::SASL, name = "failure")] pub struct Failure { /// One of the allowed defined-conditions for SASL. #[xml(child)] pub defined_condition: DefinedCondition, /// A human-readable explanation for the failure. #[xml(extract(n = .., name = "text", fields( lang(type_ = Lang, default), text(type_ = String), )))] pub texts: BTreeMap, } /// Enum which allows parsing/serialising any SASL element. #[derive(FromXml, AsXml, Debug, Clone)] #[xml()] pub enum Nonza { /// Abortion of SASL transaction #[xml(transparent)] Abort(Abort), /// Failure of SASL transaction #[xml(transparent)] Failure(Failure), /// Success of SASL transaction #[xml(transparent)] Success(Success), /// Initiation of SASL transaction #[xml(transparent)] Auth(Auth), /// Challenge sent by the server to the client #[xml(transparent)] Challenge(Challenge), /// Response sent by the client to the server #[xml(transparent)] Response(Response), } #[cfg(test)] mod tests { use super::*; use minidom::Element; #[cfg(target_pointer_width = "32")] #[test] fn test_size() { assert_size!(Mechanism, 1); assert_size!(Auth, 16); assert_size!(Challenge, 12); assert_size!(Response, 12); assert_size!(Abort, 0); assert_size!(Success, 12); assert_size!(DefinedCondition, 1); assert_size!(Failure, 16); } #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(Mechanism, 1); assert_size!(Auth, 32); assert_size!(Challenge, 24); assert_size!(Response, 24); assert_size!(Abort, 0); assert_size!(Success, 24); assert_size!(DefinedCondition, 1); assert_size!(Failure, 32); } #[test] fn test_simple() { let elem: Element = "" .parse() .unwrap(); let auth = Auth::try_from(elem).unwrap(); assert_eq!(auth.mechanism, Mechanism::Plain); assert!(auth.data.is_empty()); } #[test] fn section_6_5_1() { let elem: Element = "" .parse() .unwrap(); let failure = Failure::try_from(elem).unwrap(); assert_eq!(failure.defined_condition, DefinedCondition::Aborted); assert!(failure.texts.is_empty()); } #[test] fn section_6_5_2() { let elem: Element = " Call 212-555-1212 for assistance. " .parse() .unwrap(); let failure = Failure::try_from(elem).unwrap(); assert_eq!(failure.defined_condition, DefinedCondition::AccountDisabled); assert_eq!( failure.texts["en"], String::from("Call 212-555-1212 for assistance.") ); } /// Some servers apparently use a non-namespaced 'lang' attribute, which is invalid as not part /// of the schema. This tests whether we can parse it when disabling validation. #[cfg(feature = "disable-validation")] #[test] fn invalid_failure_with_non_prefixed_text_lang() { let elem: Element = " Invalid username or password " .parse() .unwrap(); let failure = Failure::try_from(elem).unwrap(); assert_eq!(failure.defined_condition, DefinedCondition::NotAuthorized); assert_eq!( failure.texts[""], String::from("Invalid username or password") ); } } xmpp-parsers-0.22.0/src/sasl2.rs000064400000000000000000000677561046102023000145540ustar 00000000000000// Copyright (c) 2024 Emmanuel Gil Peyrot // // 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 xso::{text::Base64, AsXml, FromXml}; use crate::bind2; use crate::ns; use crate::sm::StreamManagement; use jid::Jid; use minidom::Element; /// Server advertisement for supported auth mechanisms #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::SASL2, name = "authentication")] pub struct Authentication { /// Plaintext names of supported auth mechanisms #[xml(extract(n = .., name = "mechanism", fields(text(type_ = String))))] pub mechanisms: Vec, /// Additional auth information provided by server #[xml(child(default))] pub inline: Option, } /// Additional auth information provided by server #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::SASL2, name = "inline")] pub struct InlineFeatures { /// Bind 2 inline feature #[xml(child(default))] pub bind2: Option, /// Stream management inline feature #[xml(child(default))] pub sm: Option, /// Additional inline features #[xml(element(n = ..))] pub payloads: Vec, } /// Client aborts the connection. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::SASL2, name = "abort")] pub struct Abort { /// Plaintext reason for aborting #[xml(extract(default, fields(text(type_ = String))))] pub text: Option, /// Extra untyped payloads #[xml(element(n = ..))] pub payloads: Vec, } /// Optional client software information #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::SASL2, name = "user-agent")] pub struct UserAgent { /// Random, unique identifier for the client #[xml(attribute)] pub id: uuid::Uuid, /// Name of the client software #[xml(extract(default, fields(text(type_ = String))))] pub software: Option, /// Name of the client device (eg. phone/laptop) #[xml(extract(default, fields(text(type_ = String))))] pub device: Option, } /// Client authentication request #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::SASL2, name = "authenticate")] pub struct Authenticate { /// Chosen SASL mechanism #[xml(attribute)] pub mechanism: String, /// SASL response #[xml(extract(default, name = "initial-response", fields(text = Base64)))] pub initial_response: Option>, /// Information about client software #[xml(child)] pub user_agent: UserAgent, /// Extra untyped payloads #[xml(element(n = ..))] pub payloads: Vec, } /// SASL challenge #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::SASL2, name = "challenge")] pub struct Challenge { /// SASL challenge data #[xml(text = Base64)] pub sasl_data: Vec, } /// SASL response #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::SASL2, name = "response")] pub struct Response { /// SASL challenge data #[xml(text = Base64)] pub sasl_data: Vec, } /// Authentication was successful #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::SASL2, name = "success")] pub struct Success { /// Additional SASL data #[xml(extract(default, name = "additional-data", fields(text = Base64)))] pub additional_data: Option>, /// Identity assigned by the server #[xml(extract(name = "authorization-identifier", fields(text)))] pub authorization_identifier: Jid, /// Extra untyped payloads #[xml(element(n = ..))] pub payloads: Vec, } /// Authentication failed #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::SASL2, name = "failure")] pub struct Failure { /// Plaintext reason for failure #[xml(extract(default, fields(text(type_ = String))))] pub text: Option, /// Extra untyped payloads #[xml(element(n = ..))] pub payloads: Vec, } /// Authentication requires extra steps (eg. 2FA) #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::SASL2, name = "continue")] pub struct Continue { /// Additional SASL data #[xml(extract(name = "additional-data", fields(text = Base64)))] pub additional_data: Vec, /// List of extra authentication steps. /// /// The client may choose any, but the server may respond with more Continue steps until all required /// steps are fulfilled. #[xml(extract(fields(extract(n = .., name = "task", fields(text(type_ = String))))))] pub tasks: Vec, /// Plaintext reason for extra steps #[xml(extract(default, fields(text(type_ = String))))] pub text: Option, } /// Client answers Continue extra step by selecting task. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::SASL2, name = "next")] pub struct Next { /// Task selected by client #[xml(attribute)] pub task: String, /// Extra untyped payloads #[xml(element(n = ..))] pub payloads: Vec, } /// Client/Server data exchange about selected task. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::SASL2, name = "task-data")] pub struct TaskData { /// Extra untyped payloads #[xml(element(n = ..))] pub payloads: Vec, } #[cfg(test)] mod tests { use super::*; use base64::prelude::*; use uuid::Uuid; #[cfg(target_pointer_width = "32")] #[test] fn test_size() { assert_size!(Authentication, 40); assert_size!(InlineFeatures, 28); assert_size!(Abort, 24); assert_size!(UserAgent, 40); assert_size!(Authenticate, 76); assert_size!(Challenge, 12); assert_size!(Response, 12); assert_size!(Success, 40); assert_size!(Failure, 24); assert_size!(Continue, 36); assert_size!(Next, 24); assert_size!(TaskData, 12); } #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(Authentication, 80); assert_size!(InlineFeatures, 56); assert_size!(Abort, 48); assert_size!(UserAgent, 64); assert_size!(Authenticate, 136); assert_size!(Challenge, 24); assert_size!(Response, 24); assert_size!(Success, 80); assert_size!(Failure, 48); assert_size!(Continue, 72); assert_size!(Next, 48); assert_size!(TaskData, 24); } #[test] fn test_simple() { let elem: Element = "SCRAM-SHA-1" .parse() .unwrap(); let auth = Authentication::try_from(elem).unwrap(); assert_eq!(auth.mechanisms.len(), 1); assert_eq!(auth.inline, None); let elem: Element = "AAAA" .parse() .unwrap(); let challenge = Challenge::try_from(elem).unwrap(); assert_eq!(challenge.sasl_data, b"\0\0\0"); let elem: Element = "YWJj" .parse() .unwrap(); let response = Response::try_from(elem).unwrap(); assert_eq!(response.sasl_data, b"abc"); } // XEP-0388 Example 2 #[test] fn test_auth() { let elem: Element = r#" SCRAM-SHA-1 SCRAM-SHA-1-PLUS "# .parse() .unwrap(); let auth = Authentication::try_from(elem).unwrap(); assert_eq!(auth.mechanisms.len(), 2); let mut mech = auth.mechanisms.iter(); assert_eq!(mech.next().unwrap(), "SCRAM-SHA-1"); assert_eq!(mech.next().unwrap(), "SCRAM-SHA-1-PLUS"); assert_eq!(mech.next(), None); let inline = auth.inline.unwrap(); assert_eq!(inline.bind2.unwrap().inline_features.len(), 0); assert_eq!(inline.sm.unwrap(), StreamManagement { optional: false }); assert_eq!(inline.payloads.len(), 0); } // XEP-0388 Example 3 #[test] fn test_authenticate() { let elem: Element = r#" cD10bHMtZXhwb3J0ZXIsLG49dXNlcixyPTEyQzRDRDVDLUUzOEUtNEE5OC04RjZELTE1QzM4RjUxQ0NDNg== AwesomeXMPP Kiva's Phone "# .parse() .unwrap(); let auth = Authenticate::try_from(elem).unwrap(); assert_eq!(auth.mechanism, "SCRAM-SHA-1-PLUS"); assert_eq!( auth.initial_response.unwrap(), BASE64_STANDARD.decode("cD10bHMtZXhwb3J0ZXIsLG49dXNlcixyPTEyQzRDRDVDLUUzOEUtNEE5OC04RjZELTE1QzM4RjUxQ0NDNg==").unwrap() ); assert_eq!(auth.user_agent.software.as_ref().unwrap(), "AwesomeXMPP"); assert_eq!(auth.user_agent.device.as_ref().unwrap(), "Kiva's Phone"); } // XEP-0388 Example 4 #[test] fn test_authenticate_2() { let elem: Element = r#" SSBzaG91bGQgbWFrZSB0aGlzIGEgY29tcGV0aXRpb24= AwesomeXMPP Kiva's Phone "# .parse() .unwrap(); let auth = Authenticate::try_from(elem).unwrap(); assert_eq!(auth.mechanism, "BLURDYBLOOP"); assert_eq!( auth.initial_response.unwrap(), BASE64_STANDARD .decode("SSBzaG91bGQgbWFrZSB0aGlzIGEgY29tcGV0aXRpb24=") .unwrap() ); assert_eq!(auth.user_agent.software.as_ref().unwrap(), "AwesomeXMPP"); assert_eq!(auth.user_agent.device.as_ref().unwrap(), "Kiva's Phone"); assert_eq!(auth.payloads.len(), 1); let bind = auth.payloads.iter().next().unwrap(); assert!(bind.is("bind", "urn:xmpp:bind:example")); } // XEP-0388 Example 5 #[test] fn test_example_5() { let elem: Element = "cj0xMkM0Q0Q1Qy1FMzhFLTRBOTgtOEY2RC0xNUMzOEY1MUNDQzZhMDkxMTdhNi1hYzUwLTRmMmYtOTNmMS05Mzc5OWMyYmRkZjYscz1RU1hDUitRNnNlazhiZjkyLGk9NDA5Ng==" .parse() .unwrap(); let challenge = Challenge::try_from(elem).unwrap(); assert_eq!( challenge.sasl_data, b"r=12C4CD5C-E38E-4A98-8F6D-15C38F51CCC6a09117a6-ac50-4f2f-93f1-93799c2bddf6,s=QSXCR+Q6sek8bf92,i=4096" ); let elem: Element = "Yz1jRDEwYkhNdFpYaHdiM0owWlhJc0xNY29Rdk9kQkRlUGQ0T3N3bG1BV1YzZGcxYTFXaDF0WVBUQndWaWQxMFZVLHI9MTJDNENENUMtRTM4RS00QTk4LThGNkQtMTVDMzhGNTFDQ0M2YTA5MTE3YTYtYWM1MC00ZjJmLTkzZjEtOTM3OTljMmJkZGY2LHA9VUFwbzd4bzZQYTlKK1ZhZWpmei9kRzdCb21VPQ==" .parse() .unwrap(); let response = Response::try_from(elem).unwrap(); assert_eq!( response.sasl_data, b"c=cD10bHMtZXhwb3J0ZXIsLMcoQvOdBDePd4OswlmAWV3dg1a1Wh1tYPTBwVid10VU,r=12C4CD5C-E38E-4A98-8F6D-15C38F51CCC6a09117a6-ac50-4f2f-93f1-93799c2bddf6,p=UApo7xo6Pa9J+Vaejfz/dG7BomU=" ); } // XEP-0388 Example 7 and 8 #[test] fn test_example_7_8() { let elem: Element = r#" dj1tc1ZIcy9CeklPSERxWGVWSDdFbW1EdTlpZDg9 user@example.org "# .parse() .unwrap(); let success = Success::try_from(elem).unwrap(); assert_eq!( success.additional_data.unwrap(), BASE64_STANDARD .decode("dj1tc1ZIcy9CeklPSERxWGVWSDdFbW1EdTlpZDg9") .unwrap() ); assert_eq!( success.authorization_identifier, Jid::new("user@example.org").unwrap() ); let elem: Element = r#" ip/AeIOfZXKBV+fW2smE0GUB3I//nnrrLCYkt0Vj juliet@montague.example/Balcony/a987dsh9a87sdh "# .parse() .unwrap(); let success = Success::try_from(elem).unwrap(); assert_eq!( success.additional_data.unwrap(), BASE64_STANDARD .decode("ip/AeIOfZXKBV+fW2smE0GUB3I//nnrrLCYkt0Vj") .unwrap() ); assert_eq!( success.authorization_identifier, Jid::new("juliet@montague.example/Balcony/a987dsh9a87sdh").unwrap() ); } // XEP-0388 Example 9 #[test] fn example_success_stream_management() { let elem: Element = r#" SGFkIHlvdSBnb2luZywgdGhlcmUsIGRpZG4ndCBJPw== juliet@montague.example "# .parse() .unwrap(); let success = Success::try_from(elem).unwrap(); assert_eq!( success.additional_data.unwrap(), BASE64_STANDARD .decode("SGFkIHlvdSBnb2luZywgdGhlcmUsIGRpZG4ndCBJPw==") .unwrap() ); assert_eq!( success.authorization_identifier, Jid::new("juliet@montague.example").unwrap() ); assert_eq!(success.payloads.len(), 1); let resumed = crate::sm::Resumed::try_from(success.payloads.into_iter().next().unwrap()).unwrap(); assert_eq!(resumed.h, 345); assert_eq!(resumed.previd, crate::sm::StreamId(String::from("124"))); } // XEP-0388 Example 10 #[test] fn example_failure() { let elem: Element = r#" This is a terrible example. "# .parse() .unwrap(); let failure = Failure::try_from(elem).unwrap(); assert_eq!(failure.text.unwrap(), "This is a terrible example."); assert_eq!(failure.payloads.len(), 2); let mut payloads = failure.payloads.into_iter(); let condition = crate::sasl::DefinedCondition::try_from(payloads.next().unwrap()).unwrap(); assert_eq!(condition, crate::sasl::DefinedCondition::Aborted); assert!(payloads .next() .unwrap() .is("optional-application-specific", "urn:something:else")); } #[test] fn example_failure_no_text() { let elem: Element = r#""# .parse() .unwrap(); let failure = Failure::try_from(elem).unwrap(); assert_eq!(failure.text, None); assert_eq!(failure.payloads.len(), 1); let mut payloads = failure.payloads.into_iter(); let condition = crate::sasl::DefinedCondition::try_from(payloads.next().unwrap()).unwrap(); assert_eq!(condition, crate::sasl::DefinedCondition::Aborted); } // XEP-0388 Example 11 #[test] fn example_11() { let elem: Element = r#" SSdtIGJvcmVkIG5vdy4= HOTP-EXAMPLE TOTP-EXAMPLE This account requires 2FA "# .parse() .unwrap(); let cont = Continue::try_from(elem).unwrap(); assert_eq!( cont.additional_data, BASE64_STANDARD.decode("SSdtIGJvcmVkIG5vdy4=").unwrap() ); assert_eq!(cont.text.as_deref(), Some("This account requires 2FA")); assert_eq!(cont.tasks.len(), 2); let mut tasks = cont.tasks.into_iter(); assert_eq!(tasks.next().unwrap(), "HOTP-EXAMPLE"); assert_eq!(tasks.next().unwrap(), "TOTP-EXAMPLE"); } // XEP-0388 Example 12 #[test] fn test_fictional_totp() { let elem: Element = r#" SSd2ZSBydW4gb3V0IG9mIGlkZWFzIGhlcmUu "# .parse() .unwrap(); let next = Next::try_from(elem).unwrap(); assert_eq!(next.task, "TOTP-EXAMPLE"); let payload = next.payloads.into_iter().next().unwrap(); assert!(payload.is("totp", "urn:totp:example")); assert_eq!(&payload.text(), "SSd2ZSBydW4gb3V0IG9mIGlkZWFzIGhlcmUu"); let elem: Element = r#" 94d27acffa2e99a42ba7786162a9e73e7ab17b9d "# .parse() .unwrap(); let task_data = TaskData::try_from(elem).unwrap(); let payload = task_data.payloads.into_iter().next().unwrap(); assert!(payload.is("totp", "urn:totp:example")); assert_eq!(&payload.text(), "94d27acffa2e99a42ba7786162a9e73e7ab17b9d"); let elem: Element = r#" OTRkMjdhY2ZmYTJlOTlhNDJiYTc3ODYxNjJhOWU3M2U3YWIxN2I5ZAo= "# .parse() .unwrap(); let task_data = TaskData::try_from(elem).unwrap(); let payload = task_data.payloads.into_iter().next().unwrap(); assert!(payload.is("totp", "urn:totp:example")); assert_eq!( &payload.text(), "OTRkMjdhY2ZmYTJlOTlhNDJiYTc3ODYxNjJhOWU3M2U3YWIxN2I5ZAo=" ); let elem: Element = r#" SGFkIHlvdSBnb2luZywgdGhlcmUsIGRpZG4ndCBJPw== juliet@montague.example "# .parse() .unwrap(); let success = Success::try_from(elem).unwrap(); assert_eq!(success.additional_data, None); let payload = success.payloads.into_iter().next().unwrap(); assert!(payload.is("totp", "urn:totp:example")); assert_eq!( &payload.text(), "SGFkIHlvdSBnb2luZywgdGhlcmUsIGRpZG4ndCBJPw==" ); assert_eq!( success.authorization_identifier, Jid::new("juliet@montague.example").unwrap(), ) } /// XEP-0388 Example 13 #[test] fn example_13() { let elem: Element = r#" AGFsaWNlQGV4YW1wbGUub3JnCjM0NQ== AwesomeXMPP Kiva's Phone "# .parse() .unwrap(); let auth = Authenticate::try_from(elem).unwrap(); assert_eq!(auth.mechanism, "PLAIN"); assert_eq!( auth.initial_response.unwrap(), BASE64_STANDARD .decode("AGFsaWNlQGV4YW1wbGUub3JnCjM0NQ==") .unwrap() ); assert_eq!(auth.payloads.len(), 0); let user_agent = auth.user_agent; assert_eq!( user_agent.id, "d4565fa7-4d72-4749-b3d3-740edbf87770" .parse::() .unwrap() ); assert_eq!(user_agent.software.as_deref(), Some("AwesomeXMPP")); assert_eq!(user_agent.device.as_deref(), Some("Kiva's Phone")); let elem: Element = r#" alice@example.org "# .parse() .unwrap(); let success = Success::try_from(elem).unwrap(); assert_eq!( success.authorization_identifier, Jid::new("alice@example.org").unwrap() ); assert_eq!(success.additional_data, None); assert_eq!(success.payloads.len(), 0); } // XEP-0388 Example 14 #[test] fn example_14() { let elem: Element = r#" AwesomeXMPP Kiva's Phone "# .parse() .unwrap(); let auth = Authenticate::try_from(elem).unwrap(); assert_eq!(auth.mechanism, "CRAM-MD5"); assert_eq!(auth.initial_response, None); assert_eq!(auth.payloads.len(), 0); let user_agent = auth.user_agent; assert_eq!( user_agent.id, "d4565fa7-4d72-4749-b3d3-740edbf87770" .parse::() .unwrap() ); assert_eq!(user_agent.software.as_deref(), Some("AwesomeXMPP")); assert_eq!(user_agent.device.as_deref(), Some("Kiva's Phone")); let elem: Element = r#"PDE4OTYuNjk3MTcwOTUyQHBvc3RvZmZpY2UucmVzdG9uLm1jaS5uZXQ+"# .parse() .unwrap(); let challenge = Challenge::try_from(elem).unwrap(); assert_eq!( challenge.sasl_data, BASE64_STANDARD .decode("PDE4OTYuNjk3MTcwOTUyQHBvc3RvZmZpY2UucmVzdG9uLm1jaS5uZXQ+") .unwrap() ); let elem: Element = r#"dGltIGI5MTNhNjAyYzdlZGE3YTQ5NWI0ZTZlNzMzNGQzODkw"# .parse() .unwrap(); let response = Response::try_from(elem).unwrap(); assert_eq!( response.sasl_data, BASE64_STANDARD .decode("dGltIGI5MTNhNjAyYzdlZGE3YTQ5NWI0ZTZlNzMzNGQzODkw") .unwrap() ); let elem: Element = r#" tim@example.org "# .parse() .unwrap(); let success = Success::try_from(elem).unwrap(); assert_eq!( success.authorization_identifier, Jid::new("tim@example.org").unwrap() ); assert_eq!(success.additional_data, None); assert_eq!(success.payloads.len(), 0); } // XEP-0388 Example 15 #[test] fn example_15() { let elem: Element = r#" SW5pdGlhbCBSZXNwb25zZQ== AwesomeXMPP Kiva's Phone this-one-please "# .parse() .unwrap(); let auth = Authenticate::try_from(elem).unwrap(); assert_eq!(auth.mechanism, "BLURDYBLOOP"); assert_eq!( auth.initial_response, Some(BASE64_STANDARD.decode("SW5pdGlhbCBSZXNwb25zZQ==").unwrap()) ); assert_eq!( auth.user_agent.id, "d4565fa7-4d72-4749-b3d3-740edbf87770" .parse::() .unwrap() ); assert_eq!(auth.user_agent.software.as_deref(), Some("AwesomeXMPP")); assert_eq!(auth.user_agent.device.as_deref(), Some("Kiva's Phone")); assert_eq!(auth.payloads.len(), 1); let bind = auth.payloads.into_iter().next().unwrap(); assert!(bind.is("megabind", "urn:example:megabind")); let mut bind_payloads = bind.children(); let resource = bind_payloads.next().unwrap(); assert_eq!(resource.name(), "resource"); assert_eq!(&resource.text(), "this-one-please"); assert_eq!(bind_payloads.next(), None); let elem: Element = r#"PDE4OTYuNjk3MTcwOTUyQHBvc3RvZmZpY2UucmVzdG9uLm1jaS5uZXQ+"# .parse() .unwrap(); let challenge = Challenge::try_from(elem).unwrap(); assert_eq!( challenge.sasl_data, BASE64_STANDARD .decode("PDE4OTYuNjk3MTcwOTUyQHBvc3RvZmZpY2UucmVzdG9uLm1jaS5uZXQ+") .unwrap() ); let elem: Element = r#"dGltIGI5MTNhNjAyYzdlZGE3YTQ5NWI0ZTZlNzMzNGQzODkw"# .parse() .unwrap(); let response = Response::try_from(elem).unwrap(); assert_eq!(response.sasl_data, b"tim b913a602c7eda7a495b4e6e7334d3890"); let elem: Element = r#" QWRkaXRpb25hbCBEYXRh UNREALISTIC-2FA "# .parse() .unwrap(); let cont = Continue::try_from(elem).unwrap(); assert_eq!( cont.additional_data, BASE64_STANDARD.decode("QWRkaXRpb25hbCBEYXRh").unwrap() ); assert_eq!(cont.tasks.len(), 1); assert_eq!(cont.tasks.into_iter().next().unwrap(), "UNREALISTIC-2FA"); let elem: Element = r#" VW5yZWFsaXN0aWMgMkZBIElS "# .parse() .unwrap(); let next = Next::try_from(elem).unwrap(); assert_eq!(next.payloads.len(), 1); let params = next.payloads.into_iter().next().unwrap(); assert!(params.is("parameters", "urn:example:unrealistic2fa")); assert_eq!(¶ms.text(), "VW5yZWFsaXN0aWMgMkZBIElS"); let elem: Element = r#" PDE4OTYuNjk3MTcwOTUyQHBvc3RvZmZpY2UucmVzdG9uLm1jaS5uZXQ+ "# .parse() .unwrap(); let task_data = TaskData::try_from(elem).unwrap(); assert_eq!(task_data.payloads.len(), 1); let question = task_data.payloads.into_iter().next().unwrap(); assert!(question.is("question", "urn:example:unrealistic2fa")); assert_eq!( &question.text(), "PDE4OTYuNjk3MTcwOTUyQHBvc3RvZmZpY2UucmVzdG9uLm1jaS5uZXQ+" ); let elem: Element = r#" dGltIGI5MTNhNjAyYzdlZGE3YTQ5NWI0ZTZlNzMzNGQzODkw "# .parse() .unwrap(); let task_data = TaskData::try_from(elem).unwrap(); assert_eq!(task_data.payloads.len(), 1); let response = task_data.payloads.into_iter().next().unwrap(); assert!(response.is("response", "urn:example:unrealistic2fa")); assert_eq!( &response.text(), "dGltIGI5MTNhNjAyYzdlZGE3YTQ5NWI0ZTZlNzMzNGQzODkw" ); let elem: Element = r#" VW5yZWFsaXN0aWMgMkZBIG11dHVhbCBhdXRoIGRhdGE= alice@example.org/this-one-please "# .parse() .unwrap(); let success = Success::try_from(elem).unwrap(); assert_eq!( success.authorization_identifier, Jid::new("alice@example.org/this-one-please").unwrap() ); assert_eq!(success.payloads.len(), 1); let res = success.payloads.into_iter().next().unwrap(); assert!(res.is("result", "urn:example:unrealistic2fa")); assert_eq!(&res.text(), "VW5yZWFsaXN0aWMgMkZBIG11dHVhbCBhdXRoIGRhdGE="); } } xmpp-parsers-0.22.0/src/sasl_cb.rs000064400000000000000000000055311046102023000151150ustar 00000000000000// Copyright (c) 2024 Emmanuel Gil Peyrot // // 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 xso::{error::Error, AsXml, AsXmlText, FromXml, FromXmlText}; use crate::ns; use alloc::borrow::Cow; /// One type of channel-binding, as [defined by the IANA](https://www.iana.org/assignments/channel-binding-types/channel-binding-types.xhtml) #[derive(Debug, Clone, PartialEq)] pub enum Type { /// The tls-unique channel binding. TlsUnique, /// The tls-server-end-point channel binding. TlsServerEndPoint, /// The tls-unique-for-telnet channel binding. TlsUniqueForTelnet, /// The EKM value obtained from the current TLS connection. /// /// See RFC9266. TlsExporter, } impl FromXmlText for Type { fn from_xml_text(s: String) -> Result { Ok(match s.as_ref() { "tls-unique" => Type::TlsUnique, "tls-server-end-point" => Type::TlsServerEndPoint, "tls-unique-for-telnet" => Type::TlsUniqueForTelnet, "tls-exporter" => Type::TlsExporter, _ => return Err(Error::Other("Unknown value '{s}' for 'type' attribute.")), }) } } impl AsXmlText for Type { fn as_xml_text(&self) -> Result, Error> { Ok(Cow::Borrowed(match self { Type::TlsUnique => "tls-unique", Type::TlsServerEndPoint => "tls-server-end-point", Type::TlsUniqueForTelnet => "tls-unique-for-telnet", Type::TlsExporter => "tls-exporter", })) } } /// Stream feature listing the channel-binding types supported by the server. #[derive(FromXml, AsXml, Debug, Clone, PartialEq)] #[xml(namespace = ns::SASL_CB, name = "sasl-channel-binding")] pub struct SaslChannelBinding { /// The list of channel-binding types supported by the server. #[xml(extract(n = .., name = "channel-binding", fields(attribute(name = "type", type_ = Type))))] pub types: Vec, } #[cfg(test)] mod tests { use super::*; use minidom::Element; #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(Type, 1); assert_size!(SaslChannelBinding, 24); } #[cfg(target_pointer_width = "32")] #[test] fn test_size() { assert_size!(Type, 1); assert_size!(SaslChannelBinding, 12); } #[test] fn test_simple() { let elem: Element = "".parse().unwrap(); let sasl_cb = SaslChannelBinding::try_from(elem).unwrap(); assert_eq!(sasl_cb.types, [Type::TlsServerEndPoint, Type::TlsExporter]); } } xmpp-parsers-0.22.0/src/server_info.rs000064400000000000000000000127151046102023000160320ustar 00000000000000// Copyright (C) 2019 Maxime “pep” Buquet // 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 crate::data_forms::{DataForm, DataFormType, Field, FieldType}; use crate::ns; use xso::error::Error; /// Structure representing a `http://jabber.org/network/serverinfo` form type. #[derive(Debug, Clone, PartialEq, Default)] pub struct ServerInfo { /// Abuse addresses pub abuse: Vec, /// Admin addresses pub admin: Vec, /// Feedback addresses pub feedback: Vec, /// Sales addresses pub sales: Vec, /// Security addresses pub security: Vec, /// Support addresses pub support: Vec, } impl TryFrom for ServerInfo { type Error = Error; fn try_from(form: DataForm) -> Result { if form.type_ != DataFormType::Result_ { return Err(Error::Other("Wrong type of form.")); } if form.form_type() != Some(ns::SERVER_INFO) { return Err(Error::Other("Wrong FORM_TYPE for form.")); } let mut server_info = ServerInfo::default(); for field in form.fields { if field.var.as_deref().unwrap_or("") == "FORM_TYPE" { continue; } if field.type_ != FieldType::ListMulti { return Err(Error::Other("Field is not of the required type.")); } if field.var.as_deref() == Some("abuse-addresses") { server_info.abuse = field.values; } else if field.var.as_deref() == Some("admin-addresses") { server_info.admin = field.values; } else if field.var.as_deref() == Some("feedback-addresses") { server_info.feedback = field.values; } else if field.var.as_deref() == Some("sales-addresses") { server_info.sales = field.values; } else if field.var.as_deref() == Some("security-addresses") { server_info.security = field.values; } else if field.var.as_deref() == Some("support-addresses") { server_info.support = field.values; } else { return Err(Error::Other("Unknown form field var.")); } } Ok(server_info) } } impl From for DataForm { fn from(server_info: ServerInfo) -> DataForm { let mut form = DataForm { type_: DataFormType::Result_, title: None, instructions: None, fields: vec![ generate_address_field("abuse-addresses", server_info.abuse), generate_address_field("admin-addresses", server_info.admin), generate_address_field("feedback-addresses", server_info.feedback), generate_address_field("sales-addresses", server_info.sales), generate_address_field("security-addresses", server_info.security), generate_address_field("support-addresses", server_info.support), ], }; form.set_form_type(ns::SERVER_INFO.to_owned()); form } } /// Generate `Field` for addresses pub fn generate_address_field>(var: S, values: Vec) -> Field { Field { var: Some(var.into()), type_: FieldType::ListMulti, label: None, required: false, desc: None, options: vec![], values, media: vec![], validate: None, } } #[cfg(test)] mod tests { use super::*; #[cfg(target_pointer_width = "32")] #[test] fn test_size() { assert_size!(ServerInfo, 72); } #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(ServerInfo, 144); } #[test] fn test_simple() { let form = DataForm::new( DataFormType::Result_, ns::SERVER_INFO, vec![ Field::new("abuse-addresses", FieldType::ListMulti), Field::new("admin-addresses", FieldType::ListMulti) .with_value("xmpp:admin@foo.bar") .with_value("https://foo.bar/chat/") .with_value("mailto:admin@foo.bar"), Field::new("feedback-addresses", FieldType::ListMulti), Field::new("sales-addresses", FieldType::ListMulti), Field::new("security-addresses", FieldType::ListMulti) .with_value("xmpp:security@foo.bar") .with_value("mailto:security@foo.bar"), Field::new("support-addresses", FieldType::ListMulti) .with_value("mailto:support@foo.bar"), ], ); let server_info = ServerInfo { abuse: vec![], admin: vec![ String::from("xmpp:admin@foo.bar"), String::from("https://foo.bar/chat/"), String::from("mailto:admin@foo.bar"), ], feedback: vec![], sales: vec![], security: vec![ String::from("xmpp:security@foo.bar"), String::from("mailto:security@foo.bar"), ], support: vec![String::from("mailto:support@foo.bar")], }; // assert_eq!(DataForm::from(server_info), form); assert_eq!(ServerInfo::try_from(form).unwrap(), server_info); } } xmpp-parsers-0.22.0/src/sm.rs000064400000000000000000000220521046102023000141230ustar 00000000000000// Copyright (c) 2018 Emmanuel Gil Peyrot // // 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 xso::{AsXml, FromXml}; use crate::ns; use crate::stanza_error::DefinedCondition; /// Acknowledgement of the currently received stanzas. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::SM, name = "a")] pub struct A { /// The last handled stanza. #[xml(attribute)] pub h: u32, } impl A { /// Generates a new `` element. pub fn new(h: u32) -> A { A { h } } } /// Client request for enabling stream management. #[derive(FromXml, AsXml, PartialEq, Debug, Clone, Default)] #[xml(namespace = ns::SM, name = "enable")] pub struct Enable { /// The preferred resumption time in seconds by the client. // TODO: should be the infinite integer set ≥ 1. #[xml(attribute(default))] pub max: Option, /// Whether the client wants to be allowed to resume the stream. #[xml(attribute(default))] pub resume: bool, } impl Enable { /// Generates a new `` element. pub fn new() -> Self { Enable::default() } /// Sets the preferred resumption time in seconds. pub fn with_max(mut self, max: u32) -> Self { self.max = Some(max); self } /// Asks for resumption to be possible. pub fn with_resume(mut self) -> Self { self.resume = true; self } } generate_id!( /// A random identifier used for stream resumption. StreamId ); /// Server response once stream management is enabled. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::SM, name = "enabled")] pub struct Enabled { /// A random identifier used for stream resumption. #[xml(attribute(default))] pub id: Option, /// The preferred IP, domain, IP:port or domain:port location for /// resumption. #[xml(attribute(default))] pub location: Option, /// The preferred resumption time in seconds by the server. // TODO: should be the infinite integer set ≥ 1. #[xml(attribute(default))] pub max: Option, /// Whether stream resumption is allowed. #[xml(attribute(default))] pub resume: bool, } /// A stream management error happened. #[derive(FromXml, AsXml, Debug, PartialEq, Clone)] #[xml(namespace = ns::SM, name = "failed")] pub struct Failed { /// The last handled stanza. #[xml(attribute)] pub h: Option, /// The error returned. // XXX: implement the * handling. #[xml(child(default))] pub error: Option, } /// Requests the currently received stanzas by the other party. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::SM, name = "r")] pub struct R; /// Requests a stream resumption. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::SM, name = "resume")] pub struct Resume { /// The last handled stanza. #[xml(attribute)] pub h: u32, /// The previous id given by the server on /// [enabled](struct.Enabled.html). #[xml(attribute)] pub previd: StreamId, } /// The response by the server for a successfully resumed stream. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::SM, name = "resumed")] pub struct Resumed { /// The last handled stanza. #[xml(attribute)] pub h: u32, /// The previous id given by the server on /// [enabled](struct.Enabled.html). #[xml(attribute)] pub previd: StreamId, } /// Represents availability of Stream Management in ``. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::SM, name = "sm")] pub struct StreamManagement { /// Undocumented `` flag. // TODO: Remove this flag once servers in the wild have been updated to not send it, as it is // completely undocumented in XEP-0198, only appearing in the XML Schema before 1.6.3. #[xml(flag)] pub optional: bool, } /// Application-specific error condition to use when the peer acknowledges /// more stanzas than the local side has sent. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::SM, name = "handled-count-too-high")] pub struct HandledCountTooHigh { /// The `h` value received by the peer. #[xml(attribute)] pub h: u32, /// The number of stanzas which were in fact sent. #[xml(attribute = "send-count")] pub send_count: u32, } impl From for crate::stream_error::StreamError { fn from(other: HandledCountTooHigh) -> Self { Self::new( crate::stream_error::DefinedCondition::UndefinedCondition, "en", format!( "You acknowledged {} stanza(s), while I only sent {} so far.", other.h, other.send_count ), ) .with_application_specific(vec![other.into()]) } } /// Enum which allows parsing/serialising any XEP-0198 element. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml()] pub enum Nonza { /// Request to enable SM #[xml(transparent)] Enable(Enable), /// Successful SM enablement response #[xml(transparent)] Enabled(Enabled), /// Request to resume SM #[xml(transparent)] Resume(Resume), /// Sucessful SM resumption response #[xml(transparent)] Resumed(Resumed), /// Error response #[xml(transparent)] Failed(Failed), /// Acknowledgement #[xml(transparent)] Ack(A), /// Request for an acknowledgement #[xml(transparent)] Req(R), } #[cfg(test)] mod tests { use super::*; use minidom::Element; #[cfg(target_pointer_width = "32")] #[test] fn test_size() { assert_size!(A, 4); assert_size!(Enable, 12); assert_size!(StreamId, 12); assert_size!(Enabled, 36); assert_size!(Failed, 24); assert_size!(R, 0); assert_size!(Resume, 16); assert_size!(Resumed, 16); assert_size!(StreamManagement, 1); assert_size!(HandledCountTooHigh, 8); } #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(A, 4); assert_size!(Enable, 12); assert_size!(StreamId, 24); assert_size!(Enabled, 64); assert_size!(Failed, 40); assert_size!(R, 0); assert_size!(Resume, 32); assert_size!(Resumed, 32); assert_size!(StreamManagement, 1); assert_size!(HandledCountTooHigh, 8); } #[test] fn a() { let elem: Element = "".parse().unwrap(); let a = A::try_from(elem).unwrap(); assert_eq!(a.h, 5); } #[test] fn stream_feature() { let elem: Element = "".parse().unwrap(); StreamManagement::try_from(elem).unwrap(); } #[test] fn handle_count_too_high() { let elem: Element = "" .parse() .unwrap(); let elem = HandledCountTooHigh::try_from(elem).unwrap(); assert_eq!(elem.h, 10); assert_eq!(elem.send_count, 8); } #[test] fn resume() { let elem: Element = "" .parse() .unwrap(); let enable = Enable::try_from(elem).unwrap(); assert_eq!(enable.max, None); assert_eq!(enable.resume, true); let elem: Element = "" .parse() .unwrap(); let enabled = Enabled::try_from(elem).unwrap(); let previd = enabled.id.unwrap(); assert_eq!(enabled.resume, true); assert_eq!(previd, StreamId(String::from("coucou"))); assert_eq!(enabled.max, Some(600)); assert_eq!(enabled.location, None); let elem: Element = "" .parse() .unwrap(); let resume = Resume::try_from(elem).unwrap(); assert_eq!(resume.h, 5); assert_eq!(resume.previd, previd); let elem: Element = "" .parse() .unwrap(); let resumed = Resumed::try_from(elem).unwrap(); assert_eq!(resumed.h, 5); assert_eq!(resumed.previd, previd); } #[test] fn test_serialize_failed() { let reference: Element = "" .parse() .unwrap(); let elem: Element = "" .parse() .unwrap(); let error = DefinedCondition::try_from(elem).unwrap(); let failed = Failed { h: None, error: Some(error), }; let serialized: Element = failed.into(); assert_eq!(serialized, reference); } } xmpp-parsers-0.22.0/src/spam_reporting.rs000064400000000000000000000062521046102023000165410ustar 00000000000000// Copyright (c) 2024 Emmanuel Gil Peyrot // // 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 xso::{AsXml, FromXml}; use crate::ns; use crate::stanza_id::StanzaId; use alloc::collections::BTreeMap; generate_attribute!( /// The possible reasons for a report. Reason, "reason", { /// Used for reporting a JID that is sending unwanted messages. Spam => "urn:xmpp:reporting:spam", /// Used for reporting general abuse. Abuse => "urn:xmpp:reporting:abuse", } ); type Lang = String; /// Represents an abuse or spam report. #[derive(FromXml, AsXml, Debug, Clone, PartialEq)] #[xml(namespace = ns::SPAM_REPORTING, name = "report")] pub struct Report { /// The reason for this report. #[xml(attribute)] reason: Reason, /// Ids of the incriminated stanzas. #[xml(child(n = ..))] stanza_ids: Vec, /// Some text explaining the reason for this report. #[xml(extract(n = .., name = "text", fields( lang(type_ = Lang, default), text(type_ = String) )))] texts: BTreeMap, } #[cfg(test)] mod tests { use super::*; use jid::Jid; use minidom::Element; #[cfg(target_pointer_width = "32")] #[test] fn test_size() { assert_size!(Reason, 1); assert_size!(Report, 28); } #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(Reason, 1); assert_size!(Report, 56); } #[test] // Comes from https://xmpp.org/extensions/xep-0377.html#example-2 fn test_example_1() { let elem: Element = "" .parse() .unwrap(); let report = Report::try_from(elem).unwrap(); assert_eq!(report.reason, Reason::Spam); assert!(report.stanza_ids.is_empty()); assert!(report.texts.is_empty()); } #[test] // Comes from https://xmpp.org/extensions/xep-0377.html#example-5 fn test_example_5() { let elem: Element = " Never came trouble to my house like this. " .parse() .unwrap(); let report = Report::try_from(elem).unwrap(); let romeo = Jid::new("romeo@example.net").unwrap(); assert_eq!(report.reason, Reason::Spam); assert_eq!(report.stanza_ids.len(), 2); assert_eq!(report.stanza_ids[0].by, romeo); assert_eq!(report.stanza_ids[0].id, "28482-98726-73623"); assert_eq!(report.stanza_ids[1].by, romeo); assert_eq!(report.stanza_ids[1].id, "38383-38018-18385"); assert_eq!( report.texts["en"], "Never came trouble to my house like this." ); } } xmpp-parsers-0.22.0/src/stanza_error.rs000064400000000000000000000476351046102023000162330ustar 00000000000000// Copyright (c) 2017 Emmanuel Gil Peyrot // // 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 xso::{text::EmptyAsNone, AsXml, FromXml}; use crate::message::MessagePayload; use crate::ns; use crate::presence::PresencePayload; use alloc::collections::BTreeMap; use jid::Jid; use minidom::Element; generate_attribute!( /// The type of the error. ErrorType, "type", { /// Retry after providing credentials. Auth => "auth", /// Do not retry (the error cannot be remedied). Cancel => "cancel", /// Proceed (the condition was only a warning). Continue => "continue", /// Retry after changing the data sent. Modify => "modify", /// Retry after waiting (the error is temporary). Wait => "wait", } ); /// List of valid error conditions. // NOTE: This MUST NOT be marked as exhaustive, because the elements // use the same namespace! #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::XMPP_STANZAS)] pub enum DefinedCondition { /// The sender has sent a stanza containing XML that does not conform /// to the appropriate schema or that cannot be processed (e.g., an IQ /// stanza that includes an unrecognized value of the 'type' attribute, /// or an element that is qualified by a recognized namespace but that /// violates the defined syntax for the element); the associated error /// type SHOULD be "modify". #[xml(name = "bad-request")] BadRequest, /// Access cannot be granted because an existing resource exists with /// the same name or address; the associated error type SHOULD be /// "cancel". #[xml(name = "conflict")] Conflict, /// The feature represented in the XML stanza is not implemented by the /// intended recipient or an intermediate server and therefore the /// stanza cannot be processed (e.g., the entity understands the /// namespace but does not recognize the element name); the associated /// error type SHOULD be "cancel" or "modify". #[xml(name = "feature-not-implemented")] FeatureNotImplemented, /// The requesting entity does not possess the necessary permissions to /// perform an action that only certain authorized roles or individuals /// are allowed to complete (i.e., it typically relates to /// authorization rather than authentication); the associated error /// type SHOULD be "auth". #[xml(name = "forbidden")] Forbidden, /// The recipient or server can no longer be contacted at this address, /// typically on a permanent basis (as opposed to the \ error /// condition, which is used for temporary addressing failures); the /// associated error type SHOULD be "cancel" and the error stanza /// SHOULD include a new address (if available) as the XML character /// data of the \ element (which MUST be a Uniform Resource /// Identifier (URI) or Internationalized Resource Identifier (IRI) at /// which the entity can be contacted, typically an XMPP IRI as /// specified in [XMPP‑URI](https://www.rfc-editor.org/rfc/rfc5122)). #[xml(name = "gone")] Gone { /// The new address of the entity for which the error was returned, /// if available. #[xml(text(codec = EmptyAsNone))] new_address: Option, }, /// The server has experienced a misconfiguration or other internal /// error that prevents it from processing the stanza; the associated /// error type SHOULD be "cancel". #[xml(name = "internal-server-error")] InternalServerError, /// The addressed JID or item requested cannot be found; the associated /// error type SHOULD be "cancel". #[xml(name = "item-not-found")] ItemNotFound, /// The sending entity has provided (e.g., during resource binding) or /// communicated (e.g., in the 'to' address of a stanza) an XMPP /// address or aspect thereof that violates the rules defined in /// [XMPP‑ADDR]; the associated error type SHOULD be "modify". #[xml(name = "jid-malformed")] JidMalformed, /// The recipient or server understands the request but cannot process /// it because the request does not meet criteria defined by the /// recipient or server (e.g., a request to subscribe to information /// that does not simultaneously include configuration parameters /// needed by the recipient); the associated error type SHOULD be /// "modify". #[xml(name = "not-acceptable")] NotAcceptable, /// The recipient or server does not allow any entity to perform the /// action (e.g., sending to entities at a blacklisted domain); the /// associated error type SHOULD be "cancel". #[xml(name = "not-allowed")] NotAllowed, /// The sender needs to provide credentials before being allowed to /// perform the action, or has provided improper credentials (the name /// "not-authorized", which was borrowed from the "401 Unauthorized" /// error of HTTP, might lead the reader to think that this condition /// relates to authorization, but instead it is typically used in /// relation to authentication); the associated error type SHOULD be /// "auth". #[xml(name = "not-authorized")] NotAuthorized, /// The entity has violated some local service policy (e.g., a message /// contains words that are prohibited by the service) and the server /// MAY choose to specify the policy in the \ element or in an /// application-specific condition element; the associated error type /// SHOULD be "modify" or "wait" depending on the policy being /// violated. #[xml(name = "policy-violation")] PolicyViolation, /// The intended recipient is temporarily unavailable, undergoing /// maintenance, etc.; the associated error type SHOULD be "wait". #[xml(name = "recipient-unavailable")] RecipientUnavailable, /// The recipient or server is redirecting requests for this /// information to another entity, typically in a temporary fashion (as /// opposed to the \ error condition, which is used for permanent /// addressing failures); the associated error type SHOULD be "modify" /// and the error stanza SHOULD contain the alternate address in the /// XML character data of the \ element (which MUST be a URI /// or IRI with which the sender can communicate, typically an XMPP IRI /// as specified in [XMPP‑URI](https://xmpp.org/rfcs/rfc5122.html)). #[xml(name = "redirect")] Redirect { /// The new address of the entity for which the error was returned, /// if available. #[xml(text(codec = EmptyAsNone))] new_address: Option, }, /// The requesting entity is not authorized to access the requested /// service because prior registration is necessary (examples of prior /// registration include members-only rooms in XMPP multi-user chat /// [XEP‑0045] and gateways to non-XMPP instant messaging services, /// which traditionally required registration in order to use the /// gateway [XEP‑0100]); the associated error type SHOULD be "auth". #[xml(name = "registration-required")] RegistrationRequired, /// A remote server or service specified as part or all of the JID of /// the intended recipient does not exist or cannot be resolved (e.g., /// there is no _xmpp-server._tcp DNS SRV record, the A or AAAA /// fallback resolution fails, or A/AAAA lookups succeed but there is /// no response on the IANA-registered port 5269); the associated error /// type SHOULD be "cancel". #[xml(name = "remote-server-not-found")] RemoteServerNotFound, /// A remote server or service specified as part or all of the JID of /// the intended recipient (or needed to fulfill a request) was /// resolved but communications could not be established within a /// reasonable amount of time (e.g., an XML stream cannot be /// established at the resolved IP address and port, or an XML stream /// can be established but stream negotiation fails because of problems /// with TLS, SASL, Server Dialback, etc.); the associated error type /// SHOULD be "wait" (unless the error is of a more permanent nature, /// e.g., the remote server is found but it cannot be authenticated or /// it violates security policies). #[xml(name = "remote-server-timeout")] RemoteServerTimeout, /// The server or recipient is busy or lacks the system resources /// necessary to service the request; the associated error type SHOULD /// be "wait". #[xml(name = "resource-constraint")] ResourceConstraint, /// The server or recipient does not currently provide the requested /// service; the associated error type SHOULD be "cancel". #[xml(name = "service-unavailable")] ServiceUnavailable, /// The requesting entity is not authorized to access the requested /// service because a prior subscription is necessary (examples of /// prior subscription include authorization to receive presence /// information as defined in [XMPP‑IM] and opt-in data feeds for XMPP /// publish-subscribe as defined in [XEP‑0060]); the associated error /// type SHOULD be "auth". #[xml(name = "subscription-required")] SubscriptionRequired, /// The error condition is not one of those defined by the other /// conditions in this list; any error type can be associated with this /// condition, and it SHOULD NOT be used except in conjunction with an /// application-specific condition. #[xml(name = "undefined-condition")] UndefinedCondition, /// The recipient or server understood the request but was not /// expecting it at this time (e.g., the request was out of order); the /// associated error type SHOULD be "wait" or "modify". #[xml(name = "unexpected-request")] UnexpectedRequest, } type Lang = String; /// The representation of a stanza error. #[derive(Debug, Clone, PartialEq, FromXml, AsXml)] #[xml(namespace = ns::DEFAULT_NS, name = "error", discard(attribute = "code"))] pub struct StanzaError { /// The type of this error. #[xml(attribute = "type")] pub type_: ErrorType, /// The JID of the entity who set this error. #[xml(attribute(name = "by", default))] pub by: Option, /// One of the defined conditions for this error to happen. #[xml(child)] pub defined_condition: DefinedCondition, /// Human-readable description of this error. #[xml(extract(n = .., namespace = ns::XMPP_STANZAS, name = "text", fields( lang(type_ = Lang, default), text(type_ = String), )))] pub texts: BTreeMap, /// A protocol-specific extension for this error. #[xml(element(default))] pub other: Option, } impl MessagePayload for StanzaError {} impl PresencePayload for StanzaError {} impl StanzaError { /// Create a new `` with the according content. pub fn new( type_: ErrorType, defined_condition: DefinedCondition, lang: L, text: T, ) -> StanzaError where L: Into, T: Into, { StanzaError { type_, by: None, defined_condition, texts: { let mut map = BTreeMap::new(); map.insert(lang.into(), text.into()); map }, other: None, } } } #[cfg(test)] mod tests { use super::*; use xso::error::{Error, FromElementError}; #[cfg(target_pointer_width = "32")] #[test] fn test_size() { assert_size!(ErrorType, 1); assert_size!(DefinedCondition, 16); assert_size!(StanzaError, 108); } #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(ErrorType, 1); assert_size!(DefinedCondition, 32); assert_size!(StanzaError, 216); } #[test] fn test_simple() { #[cfg(not(feature = "component"))] let elem: Element = "".parse().unwrap(); #[cfg(feature = "component")] let elem: Element = "".parse().unwrap(); let error = StanzaError::try_from(elem).unwrap(); assert_eq!(error.type_, ErrorType::Cancel); assert_eq!( error.defined_condition, DefinedCondition::UndefinedCondition ); } #[test] fn test_invalid_type() { #[cfg(not(feature = "component"))] let elem: Element = "".parse().unwrap(); #[cfg(feature = "component")] let elem: Element = "".parse().unwrap(); let error = StanzaError::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!( message, "Required attribute field 'type_' on StanzaError element missing." ); #[cfg(not(feature = "component"))] let elem: Element = "" .parse() .unwrap(); #[cfg(feature = "component")] let elem: Element = "" .parse() .unwrap(); let error = StanzaError::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::TextParseError(string)) => string, _ => panic!(), }; assert_eq!(message.to_string(), "Unknown value for 'type' attribute."); } #[test] fn test_invalid_condition() { #[cfg(not(feature = "component"))] let elem: Element = "" .parse() .unwrap(); #[cfg(feature = "component")] let elem: Element = "" .parse() .unwrap(); let error = StanzaError::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!( message, "Missing child field 'defined_condition' in StanzaError element." ); } #[test] fn test_error_code() { #[cfg(not(feature = "component"))] let elem: Element = r#" The feature requested is not implemented by the recipient or server and therefore cannot be processed. "# .parse() .unwrap(); #[cfg(feature = "component")] let elem: Element = r#" The feature requested is not implemented by the recipient or server and therefore cannot be processed. "# .parse() .unwrap(); let stanza_error = StanzaError::try_from(elem).unwrap(); assert_eq!(stanza_error.type_, ErrorType::Cancel); } #[test] fn test_error_multiple_text() { #[cfg(not(feature = "component"))] let elem: Element = r#" Nœud non trouvé Node not found "# .parse() .unwrap(); #[cfg(feature = "component")] let elem: Element = r#" Nœud non trouvé Node not found "# .parse() .unwrap(); let stanza_error = StanzaError::try_from(elem).unwrap(); assert_eq!(stanza_error.type_, ErrorType::Cancel); } #[test] fn test_gone_with_new_address() { #[cfg(not(feature = "component"))] let elem: Element = "xmpp:room@muc.example.org?join" .parse() .unwrap(); #[cfg(feature = "component")] let elem: Element = "xmpp:room@muc.example.org?join" .parse() .unwrap(); let error = StanzaError::try_from(elem).unwrap(); assert_eq!(error.type_, ErrorType::Cancel); assert_eq!( error.defined_condition, DefinedCondition::Gone { new_address: Some("xmpp:room@muc.example.org?join".to_string()), } ); } #[test] fn test_gone_without_new_address() { #[cfg(not(feature = "component"))] let elem: Element = "" .parse() .unwrap(); #[cfg(feature = "component")] let elem: Element = "" .parse() .unwrap(); let error = StanzaError::try_from(elem).unwrap(); assert_eq!(error.type_, ErrorType::Cancel); assert_eq!( error.defined_condition, DefinedCondition::Gone { new_address: None } ); } #[test] fn test_redirect_with_alternate_address() { #[cfg(not(feature = "component"))] let elem: Element = "xmpp:characters@conference.example.org" .parse() .unwrap(); #[cfg(feature = "component")] let elem: Element = "xmpp:characters@conference.example.org" .parse() .unwrap(); let error = StanzaError::try_from(elem).unwrap(); assert_eq!(error.type_, ErrorType::Modify); assert_eq!( error.defined_condition, DefinedCondition::Redirect { new_address: Some("xmpp:characters@conference.example.org".to_string()), } ); } #[test] fn test_redirect_without_alternate_address() { #[cfg(not(feature = "component"))] let elem: Element = "" .parse() .unwrap(); #[cfg(feature = "component")] let elem: Element = "" .parse() .unwrap(); let error = StanzaError::try_from(elem).unwrap(); assert_eq!(error.type_, ErrorType::Modify); assert_eq!( error.defined_condition, DefinedCondition::Redirect { new_address: None } ); } } xmpp-parsers-0.22.0/src/stanza_id.rs000064400000000000000000000101411046102023000154540ustar 00000000000000// Copyright (c) 2017 Emmanuel Gil Peyrot // // 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 xso::{AsXml, FromXml}; use crate::message::MessagePayload; use crate::ns; use jid::Jid; /// Gives the identifier a service has stamped on this stanza, often in /// order to identify it inside of [an archive](../mam/index.html). #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::SID, name = "stanza-id")] pub struct StanzaId { /// The id associated to this stanza by another entity. #[xml(attribute)] pub id: String, /// The entity who stamped this stanza-id. #[xml(attribute)] pub by: Jid, } impl MessagePayload for StanzaId {} /// A hack for MUC before version 1.31 to track a message which may have /// its 'id' attribute changed. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::SID, name = "origin-id")] pub struct OriginId { /// The id this client set for this stanza. #[xml(attribute)] pub id: String, } impl MessagePayload for OriginId {} #[cfg(test)] mod tests { use super::*; use jid::BareJid; use minidom::Element; use xso::error::{Error, FromElementError}; #[cfg(target_pointer_width = "32")] #[test] fn test_size() { assert_size!(StanzaId, 28); assert_size!(OriginId, 12); } #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(StanzaId, 56); assert_size!(OriginId, 24); } #[test] fn test_simple() { let elem: Element = "" .parse() .unwrap(); let stanza_id = StanzaId::try_from(elem).unwrap(); assert_eq!(stanza_id.id, String::from("coucou")); assert_eq!(stanza_id.by, BareJid::new("coucou@coucou").unwrap()); let elem: Element = "" .parse() .unwrap(); let origin_id = OriginId::try_from(elem).unwrap(); assert_eq!(origin_id.id, String::from("coucou")); } #[test] #[cfg_attr(feature = "disable-validation", should_panic = "Result::unwrap_err")] fn test_invalid_child() { let elem: Element = "" .parse() .unwrap(); let error = StanzaId::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!(message, "Unknown child in StanzaId element."); } #[test] fn test_invalid_id() { let elem: Element = "".parse().unwrap(); let error = StanzaId::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!( message, "Required attribute field 'id' on StanzaId element missing." ); } #[test] fn test_invalid_by() { let elem: Element = "" .parse() .unwrap(); let error = StanzaId::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!( message, "Required attribute field 'by' on StanzaId element missing." ); } #[test] fn test_serialise() { let elem: Element = "" .parse() .unwrap(); let stanza_id = StanzaId { id: String::from("coucou"), by: Jid::new("coucou@coucou").unwrap(), }; let elem2 = stanza_id.into(); assert_eq!(elem, elem2); } } xmpp-parsers-0.22.0/src/starttls.rs000064400000000000000000000070611046102023000153670ustar 00000000000000// Copyright (c) 2024 Emmanuel Gil Peyrot // // 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 xso::{AsXml, FromXml}; use crate::ns; /// Request to start TLS. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::TLS, name = "starttls")] pub struct Request; /// Information that TLS may now commence. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::TLS, name = "proceed")] pub struct Proceed; /// Information that the peer cannot do TLS after all. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::TLS, name = "failure")] pub struct Failure; /// Stream feature for StartTLS /// /// Used in [`crate::stream_features::StreamFeatures`]. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::TLS, name = "starttls")] pub struct StartTls { /// Marker for mandatory StartTLS. #[xml(flag)] pub required: bool, } /// Enum which allows parsing/serialising any STARTTLS element. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml()] pub enum Nonza { /// Request to start TLS #[xml(transparent)] Request(Request), /// Information that TLS may now commence #[xml(transparent)] Proceed(Proceed), /// Information that the peer cannot do TLS after all. #[xml(transparent)] Failure(Failure), } #[cfg(test)] mod tests { use super::*; use minidom::Element; #[test] fn test_size() { assert_size!(Request, 0); assert_size!(Proceed, 0); assert_size!(StartTls, 1); assert_size!(Nonza, 1); } #[test] fn test_parsers() { let elem: Element = "" .parse() .unwrap(); let request = Request::try_from(elem.clone()).unwrap(); let elem2 = Element::from(request); assert_eq!(elem, elem2); let elem: Element = "" .parse() .unwrap(); let proceed = Proceed::try_from(elem.clone()).unwrap(); let elem2 = Element::from(proceed); assert_eq!(elem, elem2); let elem: Element = "" .parse() .unwrap(); let starttls = StartTls::try_from(elem.clone()).unwrap(); assert_eq!(starttls.required, false); let elem2 = Element::from(starttls); assert_eq!(elem, elem2); let elem: Element = "" .parse() .unwrap(); let starttls = StartTls::try_from(elem.clone()).unwrap(); assert_eq!(starttls.required, true); let elem2 = Element::from(starttls); assert_eq!(elem, elem2); let elem: Element = "" .parse() .unwrap(); let nonza = Nonza::try_from(elem.clone()).unwrap(); assert_eq!(nonza, Nonza::Request(Request)); let elem2 = Element::from(nonza); assert_eq!(elem, elem2); let elem: Element = "" .parse() .unwrap(); let nonza = Nonza::try_from(elem.clone()).unwrap(); assert_eq!(nonza, Nonza::Proceed(Proceed)); let elem2 = Element::from(nonza); assert_eq!(elem, elem2); } } xmpp-parsers-0.22.0/src/stream.rs000064400000000000000000000063561046102023000150100ustar 00000000000000// Copyright (c) 2018 Emmanuel Gil Peyrot // // 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 xso::{AsXml, FromXml}; use jid::BareJid; use crate::ns; /// The stream opening for client-server communications. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::STREAM, name = "stream")] pub struct Stream { /// The JID of the entity opening this stream. #[xml(attribute(default))] pub from: Option, /// The JID of the entity receiving this stream opening. #[xml(attribute(default))] to: Option, /// The id of the stream, used for authentication challenges. #[xml(attribute(default))] id: Option, /// The XMPP version used during this stream. #[xml(attribute(default))] version: Option, /// The default human language for all subsequent stanzas, which will /// be transmitted to other entities for better localisation. #[xml(lang(default))] xml_lang: Option, } impl Stream { /// Creates a simple client→server `` element. pub fn new(to: BareJid) -> Stream { Stream { from: None, to: Some(to), id: None, version: Some(String::from("1.0")), xml_lang: None, } } /// Sets the [@from](#structfield.from) attribute on this `` /// element. pub fn with_from(mut self, from: BareJid) -> Stream { self.from = Some(from); self } /// Sets the [@id](#structfield.id) attribute on this `` /// element. pub fn with_id(mut self, id: String) -> Stream { self.id = Some(id); self } /// Sets the [@xml:lang](#structfield.xml_lang) attribute on this /// `` element. pub fn with_lang(mut self, xml_lang: String) -> Stream { self.xml_lang = Some(xml_lang); self } /// Checks whether the version matches the expected one. pub fn is_version(&self, version: &str) -> bool { match self.version { None => false, Some(ref self_version) => self_version == &String::from(version), } } } #[cfg(test)] mod tests { use super::*; use minidom::Element; #[cfg(target_pointer_width = "32")] #[test] fn test_size() { assert_size!(Stream, 68); } #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(Stream, 136); } #[test] fn test_simple() { let elem: Element = "".parse().unwrap(); let stream = Stream::try_from(elem).unwrap(); assert_eq!( stream.from, Some(BareJid::new("some-server.example").unwrap()) ); assert_eq!(stream.to, None); assert_eq!(stream.id, Some(String::from("abc"))); assert_eq!(stream.version, Some(String::from("1.0"))); assert_eq!(stream.xml_lang, Some(String::from("en"))); } } xmpp-parsers-0.22.0/src/stream_error.rs000064400000000000000000000542151046102023000162160ustar 00000000000000// Copyright (c) 2024 Jonas Schäfer // // 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::collections::BTreeMap; use core::{error::Error, fmt}; use minidom::Element; use xso::{AsXml, FromXml}; use crate::{message::Lang, ns}; /// Enumeration of all stream error conditions as defined in [RFC 6120]. /// /// All variant documentation is directly quoted from [RFC 6120]. /// /// [RFC 6120]: https://datatracker.ietf.org/doc/html/rfc6120#section-4.9.3 #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::XMPP_STREAMS)] pub enum DefinedCondition { /// The entity has sent XML that cannot be processed. /// /// This error can be used instead of the more specific XML-related /// errors, such as ``, ``, /// ``, ``, and /// ``. However, the more specific errors are /// RECOMMENDED. #[xml(name = "bad-format")] BadFormat, /// The entity has sent a namespace prefix that is unsupported, or has /// sent no namespace prefix on an element that needs such a prefix (see /// [Section 11.2](https://datatracker.ietf.org/doc/html/rfc6120#section-11.2)). #[xml(name = "bad-namespace-prefix")] BadNamespacePrefix, /// The server either (1) is closing the existing stream for this entity /// because a new stream has been initiated that conflicts with the /// existing stream, or (2) is refusing a new stream for this entity /// because allowing the new stream would conflict with an existing /// stream (e.g., because the server allows only a certain number of /// connections from the same IP address or allows only one server-to- /// server stream for a given domain pair as a way of helping to ensure /// in-order processing as described under /// [Section 10.1](https://datatracker.ietf.org/doc/html/rfc6120#section-10.1)). /// /// If a client receives a `` stream error, during the resource /// binding aspect of its reconnection attempt it MUST NOT blindly request /// the resourcepart it used during the former session but instead MUST /// choose a different resourcepart; details are provided under /// [Section 7](https://datatracker.ietf.org/doc/html/rfc6120#section-7). #[xml(name = "conflict")] Conflict, /// One party is closing the stream because it has reason to believe that /// the other party has permanently lost the ability to communicate over /// the stream. The lack of ability to communicate can be discovered /// using various methods, such as whitespace keepalives as specified /// under /// [Section 4.4](https://datatracker.ietf.org/doc/html/rfc6120#section-4.4), /// XMPP-level pings as defined in /// [XEP-0199](https://xmpp.org/extensions/xep-0199.html), and /// XMPP Stream Management as defined in /// [XEP-0198](https://xmpp.org/extensions/xep-0198.html). /// /// Interoperability Note: RFC 3920 specified that the /// `` stream error is to be used if the peer has not /// generated any traffic over the stream for some period of time. /// That behavior is no longer recommended; instead, the error SHOULD be /// used only if the connected client or peer server has not responded to /// data sent over the stream. #[xml(name = "connection-timeout")] ConnectionTimeout, /// The value of the 'to' attribute provided in the initial stream header /// corresponds to an FQDN that is no longer serviced by the receiving /// entity. #[xml(name = "host-gone")] HostGone, /// The value of the 'to' attribute provided in the initial stream header /// does not correspond to an FQDN that is serviced by the receiving /// entity. #[xml(name = "host-unknown")] HostUnknown, /// A stanza sent between two servers lacks a 'to' or 'from' attribute, /// the 'from' or 'to' attribute has no value, or the value violates the /// rules for XMPP addresses /// (see [RFC 6122](https://datatracker.ietf.org/doc/html/rfc6122)). #[xml(name = "improper-addressing")] ImproperAddressing, /// The server has experienced a misconfiguration or other internal error /// that prevents it from servicing the stream. #[xml(name = "internal-server-error")] InternalServerError, /// The data provided in a 'from' attribute does not match an authorized /// JID or validated domain as negotiated (1) between two servers using /// SASL or Server Dialback, or (2) between a client and a server via /// SASL authentication and resource binding. #[xml(name = "invalid-from")] InvalidFrom, /// The stream namespace name is something other than /// `http://etherx.jabber.org/streams` (see /// [Section 11.2](https://datatracker.ietf.org/doc/html/rfc6120#section-11.2)) /// or the content namespace declared as the default namespace is not /// supported (e.g., something other than `jabber:client` or /// `jabber:server`). #[xml(name = "invalid-namespace")] InvalidNamespace, /// The entity has sent invalid XML over the stream to a server that /// performs validation (see /// [Section 11.4](https://datatracker.ietf.org/doc/html/rfc6120#section-11.4)). #[xml(name = "invalid-xml")] InvalidXml, /// The entity has attempted to send XML stanzas or other outbound data /// before the stream has been authenticated, or otherwise is not /// authorized to perform an action related to stream negotiation; the /// receiving entity MUST NOT process the offending data before sending /// the stream error. #[xml(name = "not-authorized")] NotAuthorized, /// The initiating entity has sent XML that violates the well-formedness /// rules of [XML](https://www.w3.org/TR/REC-xml/) or /// [XML-NAMES](https://www.w3.org/TR/REC-xml-names/). #[xml(name = "not-well-formed")] NotWellFormed, /// The entity has violated some local service policy (e.g., a stanza /// exceeds a configured size limit); the server MAY choose to specify /// the policy in the `` element or in an application-specific /// condition element. #[xml(name = "policy-violation")] PolicyViolation, /// The server is unable to properly connect to a remote entity that is /// needed for authentication or authorization (e.g., in certain /// scenarios related to Server Dialback /// [XEP-0220](https://xmpp.org/extensions/xep-0220.html)); this condition /// is not to be used when the cause of the error is within the /// administrative domain of the XMPP service provider, in which case the /// `` condition is more appropriate. #[xml(name = "remote-connection-failed")] RemoteConnectionFailed, /// The server is closing the stream because it has new (typically /// security-critical) features to offer, because the keys or /// certificates used to establish a secure context for the stream have /// expired or have been revoked during the life of the stream /// ([Section 13.7.2.3](https://datatracker.ietf.org/doc/html/rfc6120#section-13.7.2.3)), /// because the TLS sequence number has wrapped /// ([Section 5.3.5](https://datatracker.ietf.org/doc/html/rfc6120#section-5.3.5)), /// etc. The reset applies to the stream and to any security context /// established for that stream (e.g., via TLS and SASL), which means that /// encryption and authentication need to be negotiated again for the new /// stream (e.g., TLS session resumption cannot be used). #[xml(name = "reset")] Reset, /// The server lacks the system resources necessary to service the stream. #[xml(name = "resource-constraint")] ResourceConstraint, /// The entity has attempted to send restricted XML features such as a /// comment, processing instruction, DTD subset, or XML entity reference /// (see /// [Section 11.1](https://datatracker.ietf.org/doc/html/rfc6120#section-11.1)). #[xml(name = "restricted-xml")] RestrictedXml, /// The server will not provide service to the initiating entity but is /// redirecting traffic to another host under the administrative control /// of the same service provider. The XML character data of the /// `` element returned by the server MUST specify the /// alternate FQDN or IP address at which to connect, which MUST be a /// valid domainpart or a domainpart plus port number (separated by the /// ':' character in the form "domainpart:port"). If the domainpart is /// the same as the source domain, derived domain, or resolved IPv4 or /// IPv6 address to which the initiating entity originally connected /// (differing only by the port number), then the initiating entity /// SHOULD simply attempt to reconnect at that address. (The format of /// an IPv6 address MUST follow /// [IPv6-ADDR](https://datatracker.ietf.org/doc/html/rfc6120#ref-IPv6-ADDR), /// which includes the enclosing the IPv6 address in square brackets /// '[' and ']' as originally defined by /// [URI](https://datatracker.ietf.org/doc/html/rfc6120#ref-URI). /// ) Otherwise, the initiating entity MUST resolve the FQDN /// specified in the `` element as described under /// [Section 3.2](https://datatracker.ietf.org/doc/html/rfc6120#section-3.2). /// /// When negotiating a stream with the host to which it has been /// redirected, the initiating entity MUST apply the same policies it /// would have applied to the original connection attempt (e.g., a policy /// requiring TLS), MUST specify the same 'to' address on the initial /// stream header, and MUST verify the identity of the new host using the /// same reference identifier(s) it would have used for the original /// connection attempt (in accordance with /// [TLS-CERTS](https://datatracker.ietf.org/doc/html/rfc6120#ref-TLS-CERTS)). /// Even if the receiving entity returns a `` error /// before the confidentiality and integrity of the stream have been /// established (thus introducing the possibility of a denial-of-service /// attack), the fact that the initiating entity needs to verify the /// identity of the XMPP service based on the same reference identifiers /// implies that the initiating entity will not connect to a malicious /// entity. To reduce the possibility of a denial-of-service attack, (a) /// the receiving entity SHOULD NOT close the stream with a /// `` stream error until after the confidentiality and /// integrity of the stream have been protected via TLS or an equivalent /// security layer (such as the SASL GSSAPI mechanism), and (b) the /// receiving entity MAY have a policy of following redirects only if it /// has authenticated the receiving entity. In addition, the initiating /// entity SHOULD abort the connection attempt after a certain number of /// successive redirects (e.g., at least 2 but no more than 5). #[xml(name = "see-other-host")] SeeOtherHost(#[xml(text)] String), /// The server is being shut down and all active streams are being closed. #[xml(name = "system-shutdown")] SystemShutdown, /// The error condition is not one of those defined by the other /// conditions in this list; this error condition SHOULD NOT be used /// except in conjunction with an application-specific condition. #[xml(name = "undefined-condition")] UndefinedCondition, /// The initiating entity has encoded the stream in an encoding that is /// not supported by the server (see /// [Section 11.6](https://datatracker.ietf.org/doc/html/rfc6120#section-11.6)) /// or has otherwise improperly encoded the stream (e.g., by violating the /// rules of the /// [UTF-8](https://datatracker.ietf.org/doc/html/rfc6120#ref-UTF-8) /// encoding). #[xml(name = "unsupported-encoding")] UnsupportedEncoding, /// The receiving entity has advertised a mandatory-to-negotiate stream /// feature that the initiating entity does not support, and has offered /// no other mandatory-to-negotiate feature alongside the unsupported /// feature. #[xml(name = "unsupported-feature")] UnsupportedFeature, /// The initiating entity has sent a first-level child of the stream that /// is not supported by the server, either because the receiving entity /// does not understand the namespace or because the receiving entity /// does not understand the element name for the applicable namespace /// (which might be the content namespace declared as the default /// namespace). #[xml(name = "unsupported-stanza-type")] UnsupportedStanzaType, /// The 'version' attribute provided by the initiating entity in the /// stream header specifies a version of XMPP that is not supported by /// the server. #[xml(name = "unsupported-version")] UnsupportedVersion, } impl fmt::Display for DefinedCondition { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let s = match self { Self::BadFormat => "bad-format", Self::BadNamespacePrefix => "bad-namespace-prefix", Self::Conflict => "conflict", Self::ConnectionTimeout => "connection-timeout", Self::HostGone => "host-gone", Self::HostUnknown => "host-unknown", Self::ImproperAddressing => "improper-addressing", Self::InternalServerError => "internal-server-error", Self::InvalidFrom => "invalid-from", Self::InvalidNamespace => "invalid-namespace", Self::InvalidXml => "invalid-xml", Self::NotAuthorized => "not-authorized", Self::NotWellFormed => "not-well-formed", Self::PolicyViolation => "policy-violation", Self::RemoteConnectionFailed => "remote-connection-failed", Self::Reset => "reset", Self::ResourceConstraint => "resource-constraint", Self::RestrictedXml => "restricted-xml", Self::SeeOtherHost(ref host) => return write!(f, "see-other-host: {}", host), Self::SystemShutdown => "system-shutdown", Self::UndefinedCondition => "undefined-condition", Self::UnsupportedEncoding => "unsupported-encoding", Self::UnsupportedFeature => "unsupported-feature", Self::UnsupportedStanzaType => "unsupported-stanza-type", Self::UnsupportedVersion => "unsupported-version", }; f.write_str(s) } } /// Stream error as specified in RFC 6120. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::STREAM, name = "error")] pub struct StreamError { /// The enumerated error condition which triggered this stream error. #[xml(child)] pub condition: DefinedCondition, /// Optional error text #[xml(extract(n = .., name = "text", namespace = ns::XMPP_STREAMS, fields( lang(type_ = Lang, default), text(type_ = String), )))] pub texts: BTreeMap, /// Optional application-defined element which refines the specified /// [`Self::condition`]. // TODO: use n = 1 once we have it. #[xml(element(n = ..))] pub application_specific: Vec, } impl fmt::Display for StreamError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { ::fmt(&self.condition, f)?; if let Some((_, text)) = self.get_best_text(vec!["en"]) { write!(f, " ({:?})", text)? } if let Some(cond) = self.application_specific.first() { f.write_str(&String::from(cond))?; } Ok(()) } } impl StreamError { /// Create a new StreamError with condition, text, and language pub fn new, L: Into>( condition: DefinedCondition, lang: L, text: S, ) -> Self { let mut texts = BTreeMap::new(); texts.insert(lang.into(), text.into()); Self { condition, texts, application_specific: Vec::new(), } } /// Add a text element with the specified language pub fn add_text, S: Into>(mut self, lang: L, text: S) -> Self { self.texts.insert(lang.into(), text.into()); self } /// Append application specific element(s) pub fn with_application_specific(mut self, application_specific: Vec) -> Self { self.application_specific = application_specific; self } /// Get the best matching text from a list of preferred languages. /// /// This follows the same logic as Message::get_best_body: /// 1. First tries to find a match from the preferred languages list /// 2. Falls back to empty language ("") if available /// 3. Returns the first entry if no matches found /// /// Returns None if no text elements exist. pub fn get_best_text(&self, preferred_langs: Vec<&str>) -> Option<(Lang, &String)> { Self::get_best(&self.texts, preferred_langs) } /// Cloned variant of [`StreamError::get_best_text`] pub fn get_best_text_cloned(&self, preferred_langs: Vec<&str>) -> Option<(Lang, String)> { Self::get_best_cloned(&self.texts, preferred_langs) } // Private helper methods matching Message's pattern fn get_best<'a, T>( map: &'a BTreeMap, preferred_langs: Vec<&str>, ) -> Option<(Lang, &'a T)> { if map.is_empty() { return None; } for lang in preferred_langs { if let Some(value) = map.get(lang) { return Some((Lang::from(lang), value)); } } if let Some(value) = map.get("") { return Some((Lang::new(), value)); } map.iter().map(|(lang, value)| (lang.clone(), value)).next() } fn get_best_cloned>( map: &BTreeMap, preferred_langs: Vec<&str>, ) -> Option<(Lang, T)> { if let Some((lang, item)) = Self::get_best::(map, preferred_langs) { Some((lang, item.to_owned())) } else { None } } /// Check if the error has any text elements pub fn has_text(&self) -> bool { !self.texts.is_empty() } } /// Wrapper around [`StreamError`] which implements [`core::error::Error`] /// with an appropriate error message. #[derive(FromXml, AsXml, Debug)] #[xml(transparent)] pub struct ReceivedStreamError(pub StreamError); impl fmt::Display for ReceivedStreamError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "received stream error: {}", self.0) } } impl Error for ReceivedStreamError {} /// Wrapper around [`StreamError`] which implements [`core::error::Error`] /// with an appropriate error message. #[derive(FromXml, AsXml, Debug)] #[xml(transparent)] pub struct SentStreamError(pub StreamError); impl fmt::Display for SentStreamError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "sent stream error: {}", self.0) } } impl Error for SentStreamError {} #[cfg(test)] mod tests { use super::*; #[test] fn parses_condition_from_prosody() { let doc = ""; let err: DefinedCondition = xso::from_bytes(doc.as_bytes()).unwrap(); assert_eq!(err, DefinedCondition::UndefinedCondition); } #[test] fn parses_stream_error_from_prosody() { let doc = "No stream features to proceed with"; let err: StreamError = xso::from_bytes(doc.as_bytes()).unwrap(); assert_eq!(err.condition, DefinedCondition::UndefinedCondition); } #[test] fn test_stream_error_with_text() { let doc = br#" Server is shutting down for maintenance. "#; let err: StreamError = xso::from_bytes(doc).unwrap(); assert_eq!(err.condition, DefinedCondition::SystemShutdown); assert!(err.has_text()); let (lang, text) = err.get_best_text(vec![]).unwrap(); assert_eq!(text, "Server is shutting down for maintenance."); assert_eq!(lang, ""); } #[test] fn test_stream_error_with_multiple_languages() { let doc = br#" Message too large Nachricht zu lang "#; let err: StreamError = xso::from_bytes(doc).unwrap(); assert_eq!(err.condition, DefinedCondition::PolicyViolation); // Test German preference let (lang, text) = err.get_best_text(vec!["de"]).unwrap(); assert_eq!(lang, "de"); assert_eq!(text, "Nachricht zu lang"); // Test English preference let (lang, text) = err.get_best_text(vec!["en"]).unwrap(); assert_eq!(lang, "en"); assert_eq!(text, "Message too large"); // Test cloned variant let (lang, text) = err.get_best_text_cloned(vec!["en"]).unwrap(); assert_eq!(lang, "en"); assert_eq!(text, "Message too large"); } #[test] fn test_stream_error_constructors() { let err = StreamError::new(DefinedCondition::Reset, "en", "Connection reset"); let (lang, text) = err.get_best_text(vec!["en"]).unwrap(); assert_eq!(lang, "en"); assert_eq!(text, "Connection reset"); let err = err.add_text("de", "Verbindung zurückgesetzt"); assert_eq!(err.texts.len(), 2); } } xmpp-parsers-0.22.0/src/stream_features.rs000064400000000000000000000140211046102023000166720ustar 00000000000000// 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 minidom::Element; use xso::{AsXml, FromXml}; use crate::bind::BindFeature; use crate::ns; use crate::sasl2::Authentication; use crate::sasl_cb::SaslChannelBinding; use crate::starttls::StartTls; use crate::stream_limits::Limits; /// Wraps ``, usually the very first nonza of a /// XMPP stream. Indicates which features are supported. #[derive(FromXml, AsXml, PartialEq, Debug, Default, Clone)] #[xml(namespace = ns::STREAM, name = "features")] pub struct StreamFeatures { /// StartTLS is supported, and may be mandatory. #[xml(child(default))] pub starttls: Option, /// Bind is supported. #[xml(child(default))] pub bind: Option, /// List of supported SASL mechanisms #[xml(child(default))] pub sasl_mechanisms: SaslMechanisms, /// Limits advertised by the server. #[xml(child(default))] pub limits: Option, /// Extensible SASL Profile, a newer authentication method than the one from the RFC. #[xml(child(default))] pub sasl2: Option, /// SASL Channel-Binding Type Capability. #[xml(child(default))] pub sasl_cb: Option, /// Stream management feature #[xml(child(default))] pub stream_management: Option, /// Other stream features advertised /// /// If some features you use end up here, you may want to contribute /// a typed equivalent to the xmpp-parsers project! #[xml(element(n = ..))] pub others: Vec, } /// List of supported SASL mechanisms #[derive(FromXml, AsXml, PartialEq, Debug, Clone, Default)] #[xml(namespace = ns::SASL, name = "mechanisms")] pub struct SaslMechanisms { /// List of information elements describing this SASL mechanism. #[xml(extract(n = .., name = "mechanism", fields(text(type_ = String))))] pub mechanisms: Vec, } impl StreamFeatures { /// Can initiate TLS session with this server? pub fn can_starttls(&self) -> bool { self.starttls.is_some() } /// Does server support user resource binding? pub fn can_bind(&self) -> bool { self.bind.is_some() } } #[cfg(test)] mod tests { use super::*; use minidom::Element; #[cfg(target_pointer_width = "32")] #[test] fn test_size() { assert_size!(SaslMechanisms, 12); assert_size!(StreamFeatures, 92); } #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(SaslMechanisms, 24); assert_size!(StreamFeatures, 168); } #[test] fn test_sasl_mechanisms() { let elem: Element = " PLAIN SCRAM-SHA-1 SCRAM-SHA-1-PLUS " .parse() .unwrap(); let features = StreamFeatures::try_from(elem).unwrap(); assert_eq!( features.sasl_mechanisms.mechanisms, ["PLAIN", "SCRAM-SHA-1", "SCRAM-SHA-1-PLUS"] ); } #[test] fn test_required_starttls() { let elem: Element = " " .parse() .unwrap(); let features = StreamFeatures::try_from(elem).unwrap(); assert_eq!(features.can_bind(), false); assert_eq!(features.sasl_mechanisms.mechanisms.len(), 0); assert_eq!(features.can_starttls(), true); assert_eq!(features.starttls.unwrap().required, true); } #[test] fn test_deprecated_compression() { let elem: Element = " zlib lzw " .parse() .unwrap(); let features = StreamFeatures::try_from(elem).unwrap(); assert_eq!(features.can_bind(), true); assert_eq!(features.sasl_mechanisms.mechanisms.len(), 0); assert_eq!(features.can_starttls(), false); assert_eq!(features.others.len(), 1); let compression = &features.others[0]; assert!(compression.is("compression", "http://jabber.org/features/compress")); let mut children = compression.children(); let child = children.next().expect("zlib not found"); assert_eq!(child.name(), "method"); let mut texts = child.texts(); assert_eq!(texts.next().unwrap(), "zlib"); assert_eq!(texts.next(), None); let child = children.next().expect("lzw not found"); assert_eq!(child.name(), "method"); let mut texts = child.texts(); assert_eq!(texts.next().unwrap(), "lzw"); assert_eq!(texts.next(), None); } #[test] fn test_empty_features() { let elem: Element = "" .parse() .unwrap(); let features = StreamFeatures::try_from(elem).unwrap(); assert_eq!(features.can_bind(), false); assert_eq!(features.sasl_mechanisms.mechanisms.len(), 0); assert_eq!(features.can_starttls(), false); } } xmpp-parsers-0.22.0/src/stream_limits.rs000064400000000000000000000047101046102023000163610ustar 00000000000000// Copyright (c) 2024 Emmanuel Gil Peyrot // // 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 xso::{AsXml, FromXml}; use crate::ns; use core::num::NonZeroU32; /// Advertises limits on this stream. #[derive(FromXml, AsXml, Debug, Clone, PartialEq)] #[xml(namespace = ns::STREAM_LIMITS, name = "limits")] pub struct Limits { /// Maximum size of any first-level stream elements (including stanzas), in bytes the /// announcing entity is willing to accept. // TODO: Replace that with a direct u32 once xso supports that. #[xml(child(default))] pub max_bytes: Option, /// Number of seconds without any traffic from the iniating entity after which the server may /// consider the stream idle, and either perform liveness checks or terminate the stream. // TODO: Replace that with a direct u32 once xso supports that. #[xml(child(default))] pub idle_seconds: Option, } /// Maximum size of any first-level stream elements (including stanzas), in bytes the /// announcing entity is willing to accept. #[derive(FromXml, AsXml, Debug, Clone, PartialEq)] #[xml(namespace = ns::STREAM_LIMITS, name = "max-bytes")] pub struct MaxBytes { /// The number of bytes. #[xml(text)] pub value: NonZeroU32, } /// Number of seconds without any traffic from the iniating entity after which the server may /// consider the stream idle, and either perform liveness checks or terminate the stream. #[derive(FromXml, AsXml, Debug, Clone, PartialEq)] #[xml(namespace = ns::STREAM_LIMITS, name = "idle-seconds")] pub struct IdleSeconds { /// The number of seconds. #[xml(text)] pub value: NonZeroU32, } #[cfg(test)] mod tests { use super::*; use minidom::Element; #[test] fn test_size() { assert_size!(Limits, 8); assert_size!(MaxBytes, 4); assert_size!(IdleSeconds, 4); } #[test] fn test_simple() { let elem: Element = "262144" .parse() .unwrap(); let limits = Limits::try_from(elem).unwrap(); assert_eq!( limits.max_bytes.unwrap().value, NonZeroU32::new(262144).unwrap() ); assert!(limits.idle_seconds.is_none()); } } xmpp-parsers-0.22.0/src/time.rs000064400000000000000000000067201046102023000144460ustar 00000000000000// Copyright (c) 2019 Emmanuel Gil Peyrot // // 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 xso::{AsXml, FromXml}; use crate::date::Xep0082; use crate::iq::{IqGetPayload, IqResultPayload}; use crate::ns; use chrono::{DateTime, FixedOffset, Utc}; use core::str::FromStr; use xso::{error::Error, text::TextCodec}; struct ColonSeparatedOffset; impl TextCodec for ColonSeparatedOffset { fn decode(&self, s: String) -> Result { FixedOffset::from_str(&s).map_err(Error::text_parse_error) } fn encode<'x>(&self, value: &'x FixedOffset) -> Result>, Error> { let offset = value.local_minus_utc(); let nminutes = offset / 60; let nseconds = offset % 60; let nhours = nminutes / 60; let nminutes = nminutes % 60; if nseconds == 0 { Ok(Some(Cow::Owned(format!("{:+03}:{:02}", nhours, nminutes)))) } else { Ok(Some(Cow::Owned(format!( "{:+03}:{:02}:{:02}", nhours, nminutes, nseconds )))) } } } /// An entity time query. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::TIME, name = "time")] pub struct TimeQuery; impl IqGetPayload for TimeQuery {} /// An entity time result, containing an unique DateTime. #[derive(Debug, Clone, Copy, FromXml, AsXml)] #[xml(namespace = ns::TIME, name = "time")] pub struct TimeResult { /// The UTC offset #[xml(extract(name = "tzo", fields(text(codec = ColonSeparatedOffset))))] pub tz_offset: FixedOffset, /// The UTC timestamp #[xml(extract(name = "utc", fields(text(codec = Xep0082))))] pub utc: DateTime, } impl IqResultPayload for TimeResult {} impl From for DateTime { fn from(time: TimeResult) -> Self { time.utc.with_timezone(&time.tz_offset) } } impl From for DateTime { fn from(time: TimeResult) -> Self { time.utc } } impl From> for TimeResult { fn from(dt: DateTime) -> Self { let tz_offset = *dt.offset(); let utc = dt.with_timezone(&Utc); TimeResult { tz_offset, utc } } } impl From> for TimeResult { fn from(dt: DateTime) -> Self { TimeResult { tz_offset: FixedOffset::east_opt(0).unwrap(), utc: dt, } } } #[cfg(test)] mod tests { use super::*; use minidom::Element; // DateTime’s size doesn’t depend on the architecture. #[test] fn test_size() { assert_size!(TimeQuery, 0); assert_size!(TimeResult, 16); } #[test] fn parse_response() { let elem: Element = "" .parse() .unwrap(); let elem1 = elem.clone(); let time = TimeResult::try_from(elem).unwrap(); let dt = DateTime::::from(time); assert_eq!(dt.timezone(), FixedOffset::west_opt(6 * 3600).unwrap()); assert_eq!( dt, DateTime::::from_str("2006-12-19T12:58:35-05:00").unwrap() ); let elem2 = Element::from(time); assert_eq!(elem1, elem2); } } xmpp-parsers-0.22.0/src/tune.rs000064400000000000000000000122001046102023000144510ustar 00000000000000// Copyright (c) 2019 Emmanuel Gil Peyrot // // 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 xso::{AsXml, FromXml}; use crate::ns; use crate::pubsub::PubSubPayload; generate_elem_id!( /// The artist or performer of the song or piece. Artist, "artist", TUNE ); generate_elem_id!( /// The duration of the song or piece in seconds. Length, "length", TUNE, u16 ); generate_elem_id!( /// The user's rating of the song or piece, from 1 (lowest) to 10 (highest). Rating, "rating", TUNE, u8 ); generate_elem_id!( /// The collection (e.g., album) or other source (e.g., a band website that hosts streams or /// audio files). Source, "source", TUNE ); generate_elem_id!( /// The title of the song or piece. Title, "title", TUNE ); generate_elem_id!( /// A unique identifier for the tune; e.g., the track number within a collection or the /// specific URI for the object (e.g., a stream or audio file). Track, "track", TUNE ); generate_elem_id!( /// A URI or URL pointing to information about the song, collection, or artist. Uri, "uri", TUNE ); /// Container for formatted text. #[derive(FromXml, AsXml, Debug, Clone, PartialEq)] #[xml(namespace = ns::TUNE, name = "tune")] pub struct Tune { /// The artist or performer of the song or piece. #[xml(child(default))] artist: Option, /// The duration of the song or piece in seconds. #[xml(child(default))] length: Option, /// The user's rating of the song or piece, from 1 (lowest) to 10 (highest). #[xml(child(default))] rating: Option, /// The collection (e.g., album) or other source (e.g., a band website that hosts streams or /// audio files). #[xml(child(default))] source: Option, /// The title of the song or piece. #[xml(child(default))] title: Option, /// A unique identifier for the tune; e.g., the track number within a collection or the /// specific URI for the object (e.g., a stream or audio file). #[xml(child(default))] track: Option<Track>, /// A URI or URL pointing to information about the song, collection, or artist. #[xml(child(default))] uri: Option<Uri>, } impl PubSubPayload for Tune {} impl Tune { /// Construct an empty `<tune/>` element. pub fn new() -> Tune { Tune { artist: None, length: None, rating: None, source: None, title: None, track: None, uri: None, } } } impl Default for Tune { fn default() -> Self { Self::new() } } #[cfg(test)] mod tests { use super::*; use core::str::FromStr; use minidom::Element; #[cfg(target_pointer_width = "32")] #[test] fn test_size() { assert_size!(Tune, 68); assert_size!(Artist, 12); assert_size!(Length, 2); assert_size!(Rating, 1); assert_size!(Source, 12); assert_size!(Title, 12); assert_size!(Track, 12); assert_size!(Uri, 12); } #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(Tune, 128); assert_size!(Artist, 24); assert_size!(Length, 2); assert_size!(Rating, 1); assert_size!(Source, 24); assert_size!(Title, 24); assert_size!(Track, 24); assert_size!(Uri, 24); } #[test] fn empty() { let elem: Element = "<tune xmlns='http://jabber.org/protocol/tune'/>" .parse() .unwrap(); let elem2 = elem.clone(); let tune = Tune::try_from(elem).unwrap(); assert!(tune.artist.is_none()); assert!(tune.length.is_none()); assert!(tune.rating.is_none()); assert!(tune.source.is_none()); assert!(tune.title.is_none()); assert!(tune.track.is_none()); assert!(tune.uri.is_none()); let elem3 = tune.into(); assert_eq!(elem2, elem3); } #[test] fn full() { let elem: Element = "<tune xmlns='http://jabber.org/protocol/tune'><artist>Yes</artist><length>686</length><rating>8</rating><source>Yessongs</source><title>Heart of the Sunrise3http://www.yesworld.com/lyrics/Fragile.html#9" .parse() .unwrap(); let tune = Tune::try_from(elem).unwrap(); assert_eq!(tune.artist, Some(Artist::from_str("Yes").unwrap())); assert_eq!(tune.length, Some(Length(686))); assert_eq!(tune.rating, Some(Rating(8))); assert_eq!(tune.source, Some(Source::from_str("Yessongs").unwrap())); assert_eq!( tune.title, Some(Title::from_str("Heart of the Sunrise").unwrap()) ); assert_eq!(tune.track, Some(Track::from_str("3").unwrap())); assert_eq!( tune.uri, Some(Uri::from_str("http://www.yesworld.com/lyrics/Fragile.html#9").unwrap()) ); } } xmpp-parsers-0.22.0/src/util/macro_tests.rs000064400000000000000000002245641046102023000170200ustar 00000000000000// Copyright (c) 2024 Jonas Schäfer // // 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/. mod helpers { // we isolate the helpers into a module, because we do not want to have // them in scope below. // this is to ensure that the macros do not have hidden dependencies on // any specific names being imported. use minidom::Element; use xso::{error::Error, transform, AsXml, FromXml}; pub(super) fn roundtrip_full( s: &str, ) { let initial: Element = s.parse().unwrap(); let structural: T = match transform(&initial) { Ok(v) => v, Err(e) => panic!("failed to parse from {:?}: {}", s, e), }; let recovered = transform(&structural).expect("roundtrip did not produce an element"); assert_eq!(initial, recovered); let structural2: T = match transform(&recovered) { Ok(v) => v, Err(e) => panic!("failed to parse from serialisation of {:?}: {}", s, e), }; assert_eq!(structural, structural2); } pub(super) fn parse_str(s: &str) -> Result { let initial: Element = s.parse().unwrap(); transform(&initial) } } use self::helpers::{parse_str, roundtrip_full}; use xso::exports::rxml; use xso::{AsXml, FromXml, PrintRawXml}; // these are adverserial local names in order to trigger any issues with // unqualified names in the macro expansions. #[allow(dead_code, non_snake_case)] fn Err() {} #[allow(dead_code, non_snake_case)] fn Ok() {} #[allow(dead_code, non_snake_case)] fn Some() {} #[allow(dead_code, non_snake_case)] fn None() {} #[allow(dead_code)] type Option = ((),); #[allow(dead_code)] type Result = ((),); static NS1: &str = "urn:example:ns1"; static NS2: &str = "urn:example:ns2"; static FOO_NAME: &::xso::exports::rxml::strings::NcNameStr = { #[allow(unsafe_code)] unsafe { ::xso::exports::rxml::strings::NcNameStr::from_str_unchecked("foo") } }; static BAR_NAME: &::xso::exports::rxml::strings::NcNameStr = { #[allow(unsafe_code)] unsafe { ::xso::exports::rxml::strings::NcNameStr::from_str_unchecked("bar") } }; #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = NS1, name = "foo")] struct Empty; #[test] fn empty_roundtrip() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::(""); } #[test] fn empty_xml_name_matcher_is_specific() { assert_eq!( Empty::xml_name_matcher(), xso::fromxml::XmlNameMatcher::Specific(NS1, "foo") ); } #[test] fn empty_name_mismatch() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; match parse_str::("") { Err(xso::error::Error::TypeMismatch) => (), other => panic!("unexpected result: {:?}", other), } } #[test] fn empty_namespace_mismatch() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; match parse_str::("") { Err(xso::error::Error::TypeMismatch) => (), other => panic!("unexpected result: {:?}", other), } } #[test] #[cfg_attr( feature = "disable-validation", should_panic = "unexpected result: Ok(" )] fn empty_unexpected_attribute() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; match parse_str::("") { Err(xso::error::Error::Other(e)) => { assert_eq!(e, "Unknown attribute in Empty element."); } other => panic!("unexpected result: {:?}", other), } } #[test] #[cfg_attr( feature = "disable-validation", should_panic = "unexpected result: Ok(" )] fn empty_ignores_xml_lang() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; match parse_str::("") { Ok(Empty) => (), other => panic!("unexpected result: {:?}", other), } } #[test] #[cfg_attr( feature = "disable-validation", should_panic = "unexpected result: Ok(" )] fn empty_unexpected_child() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; match parse_str::("") { Err(xso::error::Error::Other(e)) => { assert_eq!(e, "Unknown child in Empty element."); } other => panic!("unexpected result: {:?}", other), } } #[test] fn empty_qname_check_has_precedence_over_attr_check() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; match parse_str::("") { Err(xso::error::Error::TypeMismatch) => (), other => panic!("unexpected result: {:?}", other), } } #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = NS1, name = BAR_NAME)] struct NamePath; #[test] fn name_path_roundtrip() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::(""); } #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = "urn:example:ns2", name = "baz")] struct NamespaceLit; #[test] fn namespace_lit_roundtrip() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::(""); } #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = NS1, name = "attr")] struct RequiredAttribute { #[xml(attribute)] foo: String, } #[test] fn required_attribute_roundtrip() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::(""); } #[test] fn required_attribute_positive() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; let data = parse_str::("").unwrap(); assert_eq!(data.foo, "bar"); } #[test] fn required_attribute_missing() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; match parse_str::("") { Err(::xso::error::Error::Other(e)) if e.contains("Required attribute field") && e.contains("missing") => { () } other => panic!("unexpected result: {:?}", other), } } #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = NS1, name = "attr")] struct RenamedAttribute { #[xml(attribute = "a1")] foo: String, #[xml(attribute = BAR_NAME)] bar: String, } #[test] fn renamed_attribute_roundtrip() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::(""); } #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = NS1, name = "attr")] struct NamespacedAttribute { #[xml(attribute(namespace = "urn:example:ns1", name = FOO_NAME))] foo_1: String, #[xml(attribute(namespace = NS2, name = "foo"))] foo_2: String, #[xml(attribute(namespace = NS1, name = BAR_NAME))] bar_1: String, #[xml(attribute(namespace = "urn:example:ns2", name = "bar"))] bar_2: String, } #[test] fn namespaced_attribute_roundtrip_a() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::( "", ); } #[test] fn namespaced_attribute_roundtrip_b() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::( "", ); } #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = NS1, name = "attr")] struct PrefixedAttribute { #[xml(attribute = "xml:lang")] lang: String, } #[test] fn prefixed_attribute_roundtrip() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::(""); } #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = NS1, name = "attr")] struct RequiredNonStringAttribute { #[xml(attribute)] foo: i32, } #[test] fn required_non_string_attribute_roundtrip() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::(""); } #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = NS1, name = "attr")] struct DefaultAttribute { #[xml(attribute(default))] foo: core::option::Option, #[xml(attribute(default))] bar: core::option::Option, } #[test] fn default_attribute_roundtrip_aa() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::(""); } #[test] fn default_attribute_roundtrip_pa() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::(""); } #[test] fn default_attribute_roundtrip_ap() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::(""); } #[test] fn default_attribute_roundtrip_pp() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::(""); } #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = NS1, name = "attr")] struct AttributeWithCodec { #[xml(attribute(default, codec = xso::text::EmptyAsNone))] foo: core::option::Option, } #[test] fn attribute_with_codec_is_none() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; let el = parse_str::("").unwrap(); assert_eq!(el.foo, None); let el = parse_str::("").unwrap(); assert_eq!(el.foo, None); let el = parse_str::("").unwrap(); assert_eq!(el.foo, Some(String::from("bar"))); } #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = NS1, name = "attr")] struct AttributeWithBase64Codec { #[xml(attribute(codec = xso::text::Base64))] foo: Vec, } #[test] fn attribute_with_base64_codec_roundtrip() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::(""); } #[test] fn attribute_with_base64_codec_decodes() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; let el = parse_str::("") .unwrap(); assert_eq!(el.foo, [0, 0, 0]); } #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = NS1, name = "text")] struct TextString { #[xml(text)] text: String, } #[test] fn text_string_roundtrip() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::("hello world!"); } #[test] fn text_string_positive_preserves_whitespace() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; let el = parse_str::(" \t\n").unwrap(); assert_eq!(el.text, " \t\n"); } #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = NS1, name = "text")] struct TextNonString { #[xml(text)] text: u32, } #[test] fn text_non_string_roundtrip() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::("123456"); } #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = NS1, name = "elem")] struct IgnoresWhitespaceWithoutTextConsumer; #[test] fn ignores_whitespace_without_text_consumer_positive() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; let _ = parse_str::( " \t\r\n", ) .unwrap(); } #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = NS1, name = "elem")] struct FailsTextWithoutTextConsumer; #[test] fn fails_text_without_text_consumer_positive() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; match parse_str::(" quak ") { Err(::xso::error::Error::Other(e)) if e.contains("Unexpected text") => (), other => panic!("unexpected result: {:?}", other), } } #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = NS1, name = "text")] struct TextWithCodec { #[xml(text(codec = xso::text::EmptyAsNone))] text: core::option::Option, } #[test] fn text_with_codec_roundtrip_empty() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::(""); } #[test] fn text_with_codec_roundtrip_non_empty() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::("hello"); } #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = NS1, name = "parent")] struct Parent { #[xml(child)] child: RequiredAttribute, } #[test] fn parent_roundtrip() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::("") } #[test] fn parent_positive() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; let v = parse_str::("") .unwrap(); assert_eq!(v.child.foo, "hello world!"); } #[test] fn parent_negative_duplicate_child() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; match parse_str::("") { Err(::xso::error::Error::Other(e)) if e.contains("must not have more than one") => (), other => panic!("unexpected result: {:?}", other), } } #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = NS1, name = "parent")] struct OptionalChild { #[xml(child(default))] child: core::option::Option, } #[test] fn optional_child_roundtrip_present() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::( "", ) } #[test] fn optional_child_roundtrip_absent() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::("") } #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = NS1, name = "elem")] struct BoxedChild { #[xml(child(default))] child: core::option::Option>, } #[test] fn boxed_child_roundtrip_absent() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::("") } #[test] fn boxed_child_roundtrip_nested_1() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::("") } #[test] fn boxed_child_roundtrip_nested_2() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::("") } #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = NS1, name = "elem", builder = RenamedBuilder, iterator = RenamedIter)] struct RenamedTypes; #[test] fn renamed_types_roundtrip() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::("") } #[test] #[allow(unused_comparisons)] fn renamed_types_get_renamed() { // these merely serve as a test that the types are declared with the names // given in the attributes. assert!(core::mem::size_of::() >= 0); assert!(core::mem::size_of::() >= 0); } // What is this, you may wonder? // This is a test that any generated type names won't trigger // the `non_camel_case_types` lint. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = NS1, name = "elem")] struct LintTest_; #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = NS1)] enum NameSwitchedEnum { #[xml(name = "a")] Variant1 { #[xml(attribute)] foo: String, }, #[xml(name = "b")] Variant2 { #[xml(text)] foo: String, }, } #[test] fn name_switched_enum_matcher_is_in_namespace() { assert_eq!( NameSwitchedEnum::xml_name_matcher(), xso::fromxml::XmlNameMatcher::InNamespace(NS1) ); } #[test] fn name_switched_enum_positive_variant_1() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; match parse_str::("") { Ok(NameSwitchedEnum::Variant1 { foo }) => { assert_eq!(foo, "hello"); } other => panic!("unexpected result: {:?}", other), } } #[test] fn name_switched_enum_positive_variant_2() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; match parse_str::("hello") { Ok(NameSwitchedEnum::Variant2 { foo }) => { assert_eq!(foo, "hello"); } other => panic!("unexpected result: {:?}", other), } } #[test] fn name_switched_enum_negative_name_mismatch() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; match parse_str::("hello") { Err(xso::error::Error::TypeMismatch) => (), other => panic!("unexpected result: {:?}", other), } } #[test] fn name_switched_enum_negative_namespace_mismatch() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; match parse_str::("hello") { Err(xso::error::Error::TypeMismatch) => (), other => panic!("unexpected result: {:?}", other), } } #[test] fn name_switched_enum_roundtrip_variant_1() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::("") } #[test] fn name_switched_enum_roundtrip_variant_2() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::("hello") } #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = NS1, builder = RenamedEnumBuilder, iterator = RenamedEnumIter)] enum RenamedEnumTypes { #[xml(name = "elem")] A, } #[test] fn renamed_enum_types_roundtrip() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::("") } #[test] #[allow(unused_comparisons)] fn renamed_enum_types_get_renamed() { // these merely serve as a test that the types are declared with the names // given in the attributes. assert!(core::mem::size_of::() >= 0); assert!(core::mem::size_of::() >= 0); } #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = NS1, exhaustive)] enum ExhaustiveNameSwitchedEnum { #[xml(name = "a")] Variant1 { #[xml(attribute)] foo: String, }, #[xml(name = "b")] Variant2 { #[xml(text)] foo: String, }, } #[test] fn exhaustive_name_switched_enum_negative_name_mismatch() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; match parse_str::("hello") { Err(xso::error::Error::TypeMismatch) => { panic!("unexpected result: {:?}", xso::error::Error::TypeMismatch) } Err(_) => (), other => panic!("unexpected result: {:?}", other), } } #[test] fn exhaustive_name_switched_enum_negative_namespace_mismatch() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; match parse_str::("hello") { Err(xso::error::Error::TypeMismatch) => (), other => panic!("unexpected result: {:?}", other), } } #[test] fn exhaustive_name_switched_enum_roundtrip_variant_1() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::("") } #[test] fn exhaustive_name_switched_enum_roundtrip_variant_2() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::("hello") } #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = NS1, name = "parent")] struct Children { #[xml(child(n = ..))] foo: Vec, } #[test] fn children_roundtrip() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::( "", ) } #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = NS1, name = "parent")] struct TextExtract { #[xml(extract(namespace = NS1, name = "child", fields(text)))] contents: String, } #[test] fn text_extract_positive() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; match parse_str::( "hello world", ) { Ok(TextExtract { contents }) => { assert_eq!(contents, "hello world"); } other => panic!("unexpected result: {:?}", other), } } #[test] fn text_extract_negative_absent_child() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; match parse_str::("") { Err(xso::error::Error::Other(e)) if e.contains("Missing child field") => (), other => panic!("unexpected result: {:?}", other), } } #[test] #[cfg_attr( feature = "disable-validation", should_panic = "unexpected result: Ok(" )] fn text_extract_negative_unexpected_attribute_in_child() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; match parse_str::("") { Err(xso::error::Error::Other(e)) if e.contains("Unknown attribute") => (), other => panic!("unexpected result: {:?}", other), } } #[test] #[cfg_attr( feature = "disable-validation", should_panic = "unexpected result: Ok(" )] fn text_extract_negative_unexpected_child_in_child() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; match parse_str::( "", ) { Err(xso::error::Error::Other(e)) if e.contains("Unknown child in extraction") => (), other => panic!("unexpected result: {:?}", other), } } #[test] fn text_extract_negative_duplicate_child() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; match parse_str::( "hello worldmore", ) { Err(xso::error::Error::Other(e)) if e.contains("must not have more than one") => (), other => panic!("unexpected result: {:?}", other), } } #[test] fn text_extract_roundtrip() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::( "hello world!", ) } #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = NS1, name = "parent")] struct AttributeExtract { #[xml(extract(namespace = NS1, name = "child", fields(attribute = "foo")))] contents: String, } #[test] fn attribute_extract_positive() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; match parse_str::( "", ) { Ok(AttributeExtract { contents }) => { assert_eq!(contents, "hello world"); } other => panic!("unexpected result: {:?}", other), } } #[test] fn attribute_extract_negative_absent_attribute() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; match parse_str::("") { Err(xso::error::Error::Other(e)) if e.contains("Required attribute") => (), other => panic!("unexpected result: {:?}", other), } } #[test] fn attribute_extract_negative_unexpected_text_in_child() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; match parse_str::( "fnord", ) { Err(xso::error::Error::Other(e)) if e.contains("Unexpected text") => (), other => panic!("unexpected result: {:?}", other), } } #[test] fn attribute_extract_roundtrip() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::( "", ) } #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = NS1, name = "parent")] struct OptionalAttributeExtract { #[xml(extract(namespace = NS1, name = "child", fields(attribute(name = "foo", default))))] contents: ::core::option::Option, } #[test] fn optional_attribute_extract_positive_present() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; match parse_str::( "", ) { Ok(OptionalAttributeExtract { contents: Some(contents), }) => { assert_eq!(contents, "hello world"); } other => panic!("unexpected result: {:?}", other), } } #[test] fn optional_attribute_extract_positive_present_empty() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; match parse_str::( "", ) { Ok(OptionalAttributeExtract { contents: Some(contents), }) => { assert_eq!(contents, ""); } other => panic!("unexpected result: {:?}", other), } } #[test] fn optional_attribute_extract_positive_absent() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; match parse_str::("") { Ok(OptionalAttributeExtract { contents: None }) => (), other => panic!("unexpected result: {:?}", other), } } #[test] fn optional_attribute_extract_roundtrip_present() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::( "", ) } #[test] fn optional_attribute_extract_roundtrip_present_empty() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::( "", ) } #[test] fn optional_attribute_extract_roundtrip_absent() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::("") } #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = NS1, name = "parent")] struct ChildExtract { #[xml(extract(namespace = NS1, name = "child", fields(child)))] contents: RequiredAttribute, } #[test] fn child_extract_roundtrip() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::( "", ) } #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = NS1, name = "parent")] struct NestedExtract { #[xml(extract(namespace = NS1, name = "child", fields( extract(namespace = NS1, name = "grandchild", fields(text)) )))] contents: String, } #[test] fn nested_extract_positive() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; match parse_str::( "hello world", ) { Ok(NestedExtract { contents }) => { assert_eq!(contents, "hello world"); } other => panic!("unexpected result: {:?}", other), } } #[test] fn nested_extract_roundtrip() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::("hello world") } #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = NS1, name = "parent")] struct ExtractOmitNamespace { #[xml(extract(name = "child", fields(text)))] contents: String, } #[test] fn extract_omit_namespace_roundtrip() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::( "hello world!", ) } #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = NS1, name = "parent")] struct ExtractOmitName { #[xml(extract(namespace = NS1, fields(text)))] contents: String, } #[test] fn extract_omit_name_roundtrip() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::( "hello world!", ) } #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = NS1, name = "parent")] struct ExtractOmitNameAndNamespace { #[xml(extract(fields(text)))] contents: String, } #[test] fn extract_omit_name_and_namespace_roundtrip() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::( "hello world!", ) } #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = NS1, name = "parent")] struct TextExtractVec { #[xml(extract(n = .., namespace = NS1, name = "child", fields(text(type_ = String))))] contents: Vec, } #[test] fn text_extract_vec_positive_nonempty() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; match parse_str::( "helloworld", ) { Ok(TextExtractVec { contents }) => { assert_eq!(contents[0], "hello"); assert_eq!(contents[1], "world"); assert_eq!(contents.len(), 2); } other => panic!("unexpected result: {:?}", other), } } #[test] fn text_extract_vec_positive_empty() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; match parse_str::("") { Ok(TextExtractVec { contents }) => { assert_eq!(contents.len(), 0); } other => panic!("unexpected result: {:?}", other), } } #[test] fn text_extract_vec_roundtrip() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::( "helloworld", ) } #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = NS1, name = "parent")] struct AttributeExtractVec { #[xml(extract(n = .., namespace = NS1, name = "child", fields(attribute(type_ = String, name = "attr"))))] contents: Vec, } #[test] fn text_extract_attribute_vec_positive_nonempty() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; match parse_str::( "", ) { Ok(AttributeExtractVec { contents }) => { assert_eq!(contents[0], "hello"); assert_eq!(contents[1], "world"); assert_eq!(contents.len(), 2); } other => panic!("unexpected result: {:?}", other), } } #[test] fn text_extract_attribute_vec_positive_empty() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; match parse_str::("") { Ok(AttributeExtractVec { contents }) => { assert_eq!(contents.len(), 0); } other => panic!("unexpected result: {:?}", other), } } #[test] fn text_extract_attribute_vec_roundtrip() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::( "", ) } #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = NS1, name = "parent")] struct TextOptionalExtract { #[xml(extract(namespace = NS1, name = "child", default, fields(text(type_ = String))))] contents: ::core::option::Option, } #[test] fn text_optional_extract_positive_present() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; match parse_str::( "hello world", ) { Ok(TextOptionalExtract { contents: Some(contents), }) => { assert_eq!(contents, "hello world"); } other => panic!("unexpected result: {:?}", other), } } #[test] fn text_optional_extract_positive_absent_child() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; match parse_str::("") { Ok(TextOptionalExtract { contents: None }) => (), other => panic!("unexpected result: {:?}", other), } } #[test] fn text_optional_extract_roundtrip_present() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::( "hello world!", ) } #[test] fn text_optional_extract_roundtrip_absent() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::("") } #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = NS1, name = "parent")] struct OptionalAttributeOptionalExtract { #[xml(extract(namespace = NS1, name = "child", default, fields(attribute(name = "foo", default))))] contents: ::core::option::Option, } #[test] fn optional_attribute_optional_extract_positive_present() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; match parse_str::( "", ) { Ok(OptionalAttributeOptionalExtract { contents: Some(contents), }) => { assert_eq!(contents, "hello world"); } other => panic!("unexpected result: {:?}", other), } } #[test] fn optional_attribute_optional_extract_positive_absent_attribute() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; match parse_str::( "", ) { Ok(OptionalAttributeOptionalExtract { contents: None }) => (), other => panic!("unexpected result: {:?}", other), } } #[test] fn optional_attribute_optional_extract_positive_absent_element() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; match parse_str::("") { Ok(OptionalAttributeOptionalExtract { contents: None }) => (), other => panic!("unexpected result: {:?}", other), } } #[test] fn optional_attribute_optional_extract_roundtrip_present() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::( "", ) } #[test] fn optional_attribute_optional_extract_roundtrip_absent_attribute() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::( "", ) } #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = NS1, name = "parent")] struct OptionalAttributeOptionalExtractDoubleOption { #[xml(extract(namespace = NS1, name = "child", default, fields(attribute(name = "foo", type_ = ::core::option::Option, default))))] contents: ::core::option::Option<::core::option::Option>, } #[test] fn optional_attribute_optional_extract_double_option_positive_present() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; match parse_str::( "", ) { Ok(OptionalAttributeOptionalExtractDoubleOption { contents: Some(Some(contents)), }) => { assert_eq!(contents, "hello world"); } other => panic!("unexpected result: {:?}", other), } } #[test] fn optional_attribute_optional_extract_double_option_positive_absent_attribute() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; match parse_str::( "", ) { Ok(OptionalAttributeOptionalExtractDoubleOption { contents: Some(None), }) => (), other => panic!("unexpected result: {:?}", other), } } #[test] fn optional_attribute_optional_extract_double_option_positive_absent_element() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; match parse_str::( "", ) { Ok(OptionalAttributeOptionalExtractDoubleOption { contents: None }) => (), other => panic!("unexpected result: {:?}", other), } } #[test] fn optional_attribute_optional_extract_double_option_roundtrip_present() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::( "", ) } #[test] fn optional_attribute_optional_extract_double_option_roundtrip_absent_attribute() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::( "", ) } #[test] fn optional_attribute_optional_extract_double_option_roundtrip_absent_child() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::( "", ) } #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = NS1, name = "parent")] struct ElementCatchOne { #[xml(element)] child: ::minidom::Element, } #[test] fn element_catch_one_roundtrip() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::( "", ) } #[test] fn element_catch_one_negative_none() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; match parse_str::("") { Err(::xso::error::Error::Other(e)) if e.contains("Missing child field") => (), other => panic!("unexpected result: {:?}", other), } } #[test] fn element_catch_one_negative_more_than_one_child() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; match parse_str::("") { Err(::xso::error::Error::Other(e)) if e == "Unknown child in ElementCatchOne element." => (), other => panic!("unexpected result: {:?}", other), } } #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = NS1, name = "parent")] struct ElementCatchMaybeOne { #[xml(element(default))] maybe_child: core::option::Option<::minidom::Element>, } #[test] fn element_catch_maybe_one_roundtrip_none() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::("") } #[test] fn element_catch_maybe_one_roundtrip_some() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::( "", ) } #[test] fn element_catch_maybe_one_negative_more_than_one_child() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; match parse_str::("") { Err(::xso::error::Error::Other(e)) if e == "Unknown child in ElementCatchMaybeOne element." => (), other => panic!("unexpected result: {:?}", other), } } #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = NS1, name = "parent")] struct ElementCatchChildAndOne { #[xml(child)] child: Empty, #[xml(element)] element: ::minidom::Element, } #[test] fn element_catch_child_and_one_roundtrip() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::( "", ) } #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = NS1, name = "parent")] struct ElementCatchChildAndMaybeOne { #[xml(child)] child: Empty, #[xml(element(default))] element: ::core::option::Option<::minidom::Element>, } #[test] fn element_catch_child_and_maybe_one_roundtrip() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::( "", ) } #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = NS1, name = "parent")] struct ElementCatchOneAndMany { #[xml(element)] child: ::minidom::Element, #[xml(element(n = ..))] children: Vec<::minidom::Element>, } #[test] fn element_catch_one_and_many_roundtrip() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::( "", ) } #[test] fn element_catch_one_and_many_parse_in_order() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; match parse_str::( "", ) { Ok(ElementCatchOneAndMany { child, children }) => { assert_eq!(child.attr(rxml::xml_ncname!("num")), Some("0")); assert_eq!(children.len(), 1); assert_eq!(children[0].attr(rxml::xml_ncname!("num")), Some("1")); } other => panic!("unexpected result: {:?}", other), } } #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = NS1, name = "parent")] struct ElementCatchall { #[xml(element(n = ..))] children: Vec<::minidom::Element>, } #[test] fn element_catchall_roundtrip() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::( "", ) } #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(transparent)] struct TransparentStruct(RequiredAttribute); #[test] fn transparent_struct_roundtrip() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::(""); } #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(transparent)] struct TransparentStructNamed { foo: RequiredAttribute, } #[test] fn transparent_struct_named_roundtrip() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::(""); } #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml()] enum DynamicEnum { #[xml(transparent)] A(RequiredAttribute), #[xml(namespace = NS2, name = "b")] B { #[xml(text)] contents: String, }, } #[test] fn dynamic_enum_matcher_is_any() { assert_eq!( DynamicEnum::xml_name_matcher(), xso::fromxml::XmlNameMatcher::Any ); } #[test] fn dynamic_enum_roundtrip_a() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::(""); } #[test] fn dynamic_enum_roundtrip_b() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::("hello world"); } #[derive(FromXml, Debug)] #[xml(namespace = NS1, name = "parent")] struct FallibleParse { #[xml(child)] child: ::core::result::Result, } #[test] fn fallible_parse_positive_ok() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; match parse_str::("") { Ok(FallibleParse { child: Ok(RequiredAttribute { foo }), }) => { assert_eq!(foo, "bar"); } other => panic!("unexpected result: {:?}", other), } } #[test] fn fallible_parse_positive_err() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; match parse_str::("") { Ok(FallibleParse { child: Err(e) }) => { assert!(e.to_string().contains("attribute")); } other => panic!("unexpected result: {:?}", other), } } #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml()] enum DynamicEnumWithSharedNamespace { #[xml(transparent)] A(RequiredAttribute), #[xml(namespace = NS1, name = "b")] B { #[xml(text)] contents: String, }, } #[test] fn dynamic_enum_with_shared_namespace_matcher_is_in_namespace() { assert_eq!( DynamicEnumWithSharedNamespace::xml_name_matcher(), xso::fromxml::XmlNameMatcher::InNamespace(NS1) ); } #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = NS1, name = "parent")] struct ExtractTupleToCollection { #[xml(extract(n = .., namespace = NS1, name = "text", fields( attribute(name = "xml:lang", type_ = ::core::option::Option, default), text(type_ = String), )))] contents: Vec<(::core::option::Option, String)>, } #[test] fn extract_tuple_to_collection_roundtrip() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::( "hello worldhallo welt", ); } #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = NS1, name = "parent")] struct ExtractTuple { #[xml(extract(namespace = NS1, name = "text", fields( attribute(name = "xml:lang", type_ = ::core::option::Option, default), text(type_ = String), )))] contents: (::core::option::Option, String), } #[test] fn extract_tuple_roundtrip() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::( "hallo welt", ); } #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = NS1, name = "parent")] struct ExtractOptionalTuple { #[xml(extract(namespace = NS1, name = "text", default, fields( attribute(name = "xml:lang", type_ = ::core::option::Option, default), text(type_ = String), )))] contents: ::core::option::Option<(::core::option::Option, String)>, } #[test] fn extract_optional_tuple_roundtrip_present() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::( "hallo welt", ); // Cannot test without text body here right now due to a limitation in the // `#[xml(text)]` serialiser: It will create an empty text node in the // minidom output, which will then fail the comparison. roundtrip_full::( "x", ); roundtrip_full::( "x", ); } #[test] fn extract_optional_tuple_roundtrip_absent() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::(""); } #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = NS1, name = "parent")] struct ExtractTupleToMap { #[xml(extract(n = .., namespace = NS1, name = "text", fields( attribute(name = "xml:lang", type_ = ::core::option::Option, default), text(type_ = String), )))] contents: alloc::collections::BTreeMap<::core::option::Option, String>, } #[test] fn extract_tuple_to_map_roundtrip() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::( "hello worldhallo welt", ); } #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = NS1, name = "foo", on_unknown_attribute = Discard)] struct IgnoreUnknownAttributes; #[test] fn ignore_unknown_attributes_empty_roundtrip() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::(""); } #[test] fn ignore_unknown_attributes_positive() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; match parse_str::("") { Ok(IgnoreUnknownAttributes) => (), other => panic!("unexpected result: {:?}", other), } } #[test] #[cfg_attr( feature = "disable-validation", should_panic = "unexpected result: Ok(" )] fn ignore_unknown_attributes_negative_unexpected_child() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; match parse_str::("") { Err(xso::error::Error::Other(e)) => { assert_eq!(e, "Unknown child in IgnoreUnknownAttributes element."); } other => panic!("unexpected result: {:?}", other), } } #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = NS1, name = "foo", on_unknown_child = Discard)] struct IgnoreUnknownChildren; #[test] fn ignore_unknown_children_empty_roundtrip() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::(""); } #[test] fn ignore_unknown_children_positive() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; match parse_str::("") { Ok(IgnoreUnknownChildren) => (), other => panic!("unexpected result: {:?}", other), } } #[test] #[cfg_attr( feature = "disable-validation", should_panic = "unexpected result: Ok(" )] fn ignore_unknown_children_negative_unexpected_attribute() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; match parse_str::("") { Err(xso::error::Error::Other(e)) => { assert_eq!(e, "Unknown attribute in IgnoreUnknownChildren element."); } other => panic!("unexpected result: {:?}", other), } } #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = NS1, name = "parent")] struct ExtractIgnoreUnknownStuff { #[xml(extract(namespace = NS1, name = "child", on_unknown_attribute = Discard, on_unknown_child = Discard, fields( extract(namespace = NS1, name = "grandchild", fields(text)) )))] contents: String, } #[test] fn extract_ignore_unknown_stuff_positive() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; match parse_str::( "hello world", ) { Ok(ExtractIgnoreUnknownStuff { contents }) => { assert_eq!(contents, "hello world"); } other => panic!("unexpected result: {:?}", other), } } #[test] fn extract_ignore_unknown_stuff_roundtrip() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::("hello world") } #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = NS1, name = "foo")] struct Flag { #[xml(flag)] flag: bool, } #[test] fn flag_parse_present_as_true() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; match parse_str::("") { Ok(Flag { flag }) => { assert!(flag); } other => panic!("unexpected result: {:?}", other), } } #[test] fn flag_present_roundtrip() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::(""); } #[test] fn flag_absent_roundtrip() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::(""); } #[test] fn printrawxml() { let text = TextString { text: String::from("hello world"), }; let display = format!("{}", PrintRawXml(&text)); assert_eq!(display, "hello world"); } #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = NS1, name = "foo", discard(attribute = "bar"))] struct DiscardAttribute; #[test] fn discard_attribute_ignore_if_present() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; match parse_str::("") { Ok(DiscardAttribute) => (), other => panic!("unexpected result: {:?}", other), } } #[test] fn discard_attribute_ignore_if_absent() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; match parse_str::("") { Ok(DiscardAttribute) => (), other => panic!("unexpected result: {:?}", other), } } #[test] fn discard_attribute_absent_roundtrip() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::(""); } #[test] #[cfg_attr( feature = "disable-validation", should_panic = "unexpected result: Ok(" )] fn discard_attribute_fails_on_other_unexpected_attributes() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; match parse_str::("") { Err(xso::error::Error::Other(e)) => { assert_eq!(e, "Unknown attribute in DiscardAttribute element."); } other => panic!("unexpected result: {:?}", other), } } #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = NS1, name = "foo", discard(text))] struct DiscardText; #[test] fn discard_text_ignore_if_present() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; match parse_str::("quak") { Ok(DiscardText) => (), other => panic!("unexpected result: {:?}", other), } } #[test] fn discard_text_ignore_if_absent() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; match parse_str::("") { Ok(DiscardText) => (), other => panic!("unexpected result: {:?}", other), } } #[test] fn discard_text_absent_roundtrip() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::(""); } fn transform_test_struct( v: &mut DeserializeCallback, ) -> ::core::result::Result<(), ::xso::error::Error> { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; use xso::error::Error; if v.outcome == 0 { return Err(Error::Other("saw outcome == 0")); } if v.outcome == 1 { v.outcome = 0; } Ok(()) } #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = NS1, name = "foo", deserialize_callback = transform_test_struct)] struct DeserializeCallback { #[xml(attribute)] outcome: u32, } #[test] fn deserialize_callback_roundtrip() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::(""); } #[test] fn deserialize_callback_can_mutate() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; match parse_str::("") { Ok(DeserializeCallback { outcome }) => { assert_eq!(outcome, 0); } other => panic!("unexpected result: {:?}", other), } } #[test] fn deserialize_callback_can_fail() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; match parse_str::("") { Err(xso::error::Error::Other(e)) => { assert_eq!(e, "saw outcome == 0"); } other => panic!("unexpected result: {:?}", other), } } fn transform_test_enum( v: &mut DeserializeCallbackEnum, ) -> ::core::result::Result<(), ::xso::error::Error> { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; use xso::error::Error; match v { DeserializeCallbackEnum::Foo { ref mut outcome } => { if *outcome == 0 { return Err(Error::Other("saw outcome == 0")); } if *outcome == 1 { *outcome = 0; } Ok(()) } } } #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = NS1, deserialize_callback = transform_test_enum)] enum DeserializeCallbackEnum { #[xml(name = "foo")] Foo { #[xml(attribute)] outcome: u32, }, } #[test] fn enum_deserialize_callback_roundtrip() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::(""); } #[test] fn enum_deserialize_callback_can_mutate() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; match parse_str::("") { Ok(DeserializeCallbackEnum::Foo { outcome }) => { assert_eq!(outcome, 0); } other => panic!("unexpected result: {:?}", other), } } #[test] fn enum_deserialize_callback_can_fail() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; match parse_str::("") { Err(xso::error::Error::Other(e)) => { assert_eq!(e, "saw outcome == 0"); } other => panic!("unexpected result: {:?}", other), } } /// This struct failed to compile at some point, failing to find the /// (internally generated) identifier `fid`. #[derive(AsXml, FromXml, PartialEq, Debug, Clone)] #[xml(namespace = NS1, name = "thread")] struct TextVsAttributeOrderingCompileBug { #[xml(text)] id: String, #[xml(attribute(default))] parent: ::core::option::Option, } #[test] fn text_vs_attribute_ordering_compile_bug_roundtrip() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::( "foo", ); } #[test] fn text_vs_attribute_ordering_compile_bug_roundtrip_with_parent() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::( "foo", ); } #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = NS1, name = "elem")] struct Language { #[xml(child(default))] child: core::option::Option>, #[xml(lang(default))] lang: core::option::Option, } #[test] fn language_roundtrip_absent() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::(""); } #[test] fn language_roundtrip_present() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::(""); } #[test] fn language_roundtrip_nested_parse() { // cannot write a round-trip test for this, because on emission, // `#[xml(language)]` fields with a non-None value will always emit // the `xml:lang` attribute to ensure semantic correctness. #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; match parse_str::( "", ) { Ok(Language { child, lang }) => { assert_eq!(lang.as_deref(), Some("foo")); let Some(Language { child, lang }) = child.map(|x| *x) else { panic!("missing child"); }; assert_eq!(lang.as_deref(), Some("foo")); let Some(Language { child, lang }) = child.map(|x| *x) else { panic!("missing grand child"); }; assert_eq!(lang.as_deref(), Some("bar")); assert!(child.is_none()); } other => panic!("unexpected parse result: {:?}", other), } } #[derive(FromXml, AsXml, Debug, Clone, PartialEq)] #[xml(namespace = NS1, name = "foo", attribute = "bar", exhaustive)] enum AttributeSwitchedEnum { #[xml(value = "a")] A { #[xml(attribute = "baz")] baz: String, }, #[xml(value = "b")] B { #[xml(text)] content: String, }, } #[test] fn attribute_switched_enum_roundtrip_a() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::(""); } #[test] fn attribute_switched_enum_roundtrip_b() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; roundtrip_full::("abc"); } #[test] fn attribute_switched_enum_negative_namespace_mismatch() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; match parse_str::("abc") { Err(xso::error::Error::TypeMismatch) => (), other => panic!("unexpected result: {:?}", other), } } #[test] fn attribute_switched_enum_negative_name_mismatch() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; match parse_str::("abc") { Err(xso::error::Error::TypeMismatch) => (), other => panic!("unexpected result: {:?}", other), } } #[test] fn attribute_switched_enum_negative_attribute_missing() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; match parse_str::("") { Err(xso::error::Error::Other(e)) => { assert_eq!(e, "Missing discriminator attribute."); } other => panic!("unexpected result: {:?}", other), } } #[test] fn attribute_switched_enum_negative_attribute_mismatch() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; match parse_str::("") { Err(xso::error::Error::Other(e)) => { assert_eq!(e, "Unknown value for discriminator attribute."); } other => panic!("unexpected result: {:?}", other), } } #[test] fn attribute_switched_enum_positive_attribute_mismatch() { #[allow(unused_imports)] use core::{ option::Option::{None, Some}, result::Result::{Err, Ok}, }; match parse_str::("abc") { Ok(AttributeSwitchedEnum::B { content }) => { assert_eq!(content, "abc"); } other => panic!("unexpected result: {:?}", other), } } xmpp-parsers-0.22.0/src/util/macros.rs000064400000000000000000000350461046102023000157540ustar 00000000000000// Copyright (c) 2017-2018 Emmanuel Gil Peyrot // // 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/. macro_rules! get_attr { ($elem:ident, $attr:tt, $type:tt) => { get_attr!( $elem, $attr, $type, value, value.parse().map_err(xso::error::Error::text_parse_error)? ) }; ($elem:ident, $attr:tt, Option, $value:ident, $func:expr) => { match $elem.attr($attr) { Some($value) => Some($func), None => None, } }; ($elem:ident, $attr:tt, Required, $value:ident, $func:expr) => { match $elem.attr($attr) { Some($value) => $func, None => { return Err(xso::error::Error::Other( concat!("Required attribute '", $attr, "' missing.").into(), ) .into()); } } }; ($elem:ident, $attr:tt, Default, $value:ident, $func:expr) => { match $elem.attr($attr) { Some($value) => $func, None => ::core::default::Default::default(), } }; } macro_rules! generate_attribute { ($(#[$meta:meta])* $elem:ident, $name:tt, {$($(#[$a_meta:meta])* $a:ident => $b:tt),+$(,)?}) => ( $(#[$meta])* #[derive(Debug, Clone, PartialEq)] pub enum $elem { $( $(#[$a_meta])* $a ),+ } impl ::core::str::FromStr for $elem { type Err = xso::error::Error; fn from_str(s: &str) -> Result<$elem, xso::error::Error> { Ok(match s { $($b => $elem::$a),+, _ => return Err(xso::error::Error::Other(concat!("Unknown value for '", $name, "' attribute.")).into()), }) } } impl ::xso::FromXmlText for $elem { fn from_xml_text(s: String) -> Result<$elem, xso::error::Error> { s.parse().map_err(xso::error::Error::text_parse_error) } } impl core::fmt::Display for $elem { fn fmt(&self, fmt: &mut core::fmt::Formatter) -> Result<(), core::fmt::Error> { write!(fmt, "{}", match self { $($elem::$a => $b),+ }) } } impl ::xso::AsXmlText for $elem { fn as_xml_text(&self) -> Result<::alloc::borrow::Cow<'_, str>, xso::error::Error> { match self { $( $elem::$a => Ok(::alloc::borrow::Cow::Borrowed($b)) ),+ } } } impl ::minidom::IntoAttributeValue for $elem { fn into_attribute_value(self) -> Option { Some(String::from(match self { $($elem::$a => $b),+ })) } } ); ($(#[$meta:meta])* $elem:ident, $name:tt, {$($(#[$a_meta:meta])* $a:ident => $b:tt),+$(,)?}, Default = $default:ident) => ( $(#[$meta])* #[derive(Debug, Clone, PartialEq)] pub enum $elem { $( $(#[$a_meta])* $a ),+ } impl ::core::str::FromStr for $elem { type Err = xso::error::Error; fn from_str(s: &str) -> Result<$elem, xso::error::Error> { Ok(match s { $($b => $elem::$a),+, _ => return Err(xso::error::Error::Other(concat!("Unknown value for '", $name, "' attribute.")).into()), }) } } impl ::xso::FromXmlText for $elem { fn from_xml_text(s: String) -> Result<$elem, xso::error::Error> { s.parse().map_err(xso::error::Error::text_parse_error) } } impl ::xso::AsXmlText for $elem { fn as_xml_text(&self) -> Result, xso::error::Error> { Ok(alloc::borrow::Cow::Borrowed(match self { $($elem::$a => $b),+ })) } #[allow(unreachable_patterns)] fn as_optional_xml_text(&self) -> Result>, xso::error::Error> { Ok(Some(alloc::borrow::Cow::Borrowed(match self { $elem::$default => return Ok(None), $($elem::$a => $b),+ }))) } } impl ::minidom::IntoAttributeValue for $elem { #[allow(unreachable_patterns)] fn into_attribute_value(self) -> Option { Some(String::from(match self { $elem::$default => return None, $($elem::$a => $b),+ })) } } impl ::core::default::Default for $elem { fn default() -> $elem { $elem::$default } } ); ($(#[$meta:meta])* $elem:ident, $name:tt, ($(#[$meta_symbol:meta])* $symbol:ident => $value:tt)) => ( $(#[$meta])* #[derive(Debug, Clone, PartialEq)] pub enum $elem { $(#[$meta_symbol])* $symbol, /// Value when absent. None, } impl ::core::str::FromStr for $elem { type Err = xso::error::Error; fn from_str(s: &str) -> Result { Ok(match s { $value => $elem::$symbol, _ => return Err(xso::error::Error::Other(concat!("Unknown value for '", $name, "' attribute."))), }) } } impl ::minidom::IntoAttributeValue for $elem { fn into_attribute_value(self) -> Option { match self { $elem::$symbol => Some(String::from($value)), $elem::None => None } } } impl ::core::default::Default for $elem { fn default() -> $elem { $elem::None } } impl ::xso::FromXmlText for $elem { fn from_xml_text(s: String) -> Result<$elem, xso::error::Error> { s.parse().map_err(xso::error::Error::text_parse_error) } } impl ::xso::AsXmlText for $elem { fn as_xml_text(&self) -> Result<::alloc::borrow::Cow<'_, str>, xso::error::Error> { Ok(::alloc::borrow::Cow::Borrowed($value)) } #[allow(unreachable_patterns)] fn as_optional_xml_text(&self) -> Result>, xso::error::Error> { Ok(Some(alloc::borrow::Cow::Borrowed(match self { $elem::$symbol => $value, $elem::None => return Ok(None), }))) } } ); ($(#[$meta:meta])* $elem:ident, $name:tt, $type:tt, Default = $default:expr) => ( $(#[$meta])* #[derive(Debug, Clone, PartialEq)] pub struct $elem(pub $type); impl ::core::str::FromStr for $elem { type Err = xso::error::Error; fn from_str(s: &str) -> Result { Ok($elem($type::from_str(s).map_err(xso::error::Error::text_parse_error)?)) } } impl ::minidom::IntoAttributeValue for $elem { fn into_attribute_value(self) -> Option { match self { $elem($default) => None, $elem(value) => Some(format!("{}", value)), } } } impl ::core::default::Default for $elem { fn default() -> $elem { $elem($default) } } impl ::xso::FromXmlText for $elem { fn from_xml_text(s: String) -> Result<$elem, xso::error::Error> { s.parse().map_err(xso::error::Error::text_parse_error) } } impl ::xso::AsXmlText for $elem { fn as_xml_text(&self) -> Result<::alloc::borrow::Cow<'_, str>, xso::error::Error> { Ok(::alloc::borrow::Cow::Owned(format!("{}", self.0))) } fn as_optional_xml_text(&self) -> Result>, xso::error::Error> { match self.0 { $default => Ok(None), _ => Ok(Some(::alloc::borrow::Cow::Owned(format!("{}", self.0)))), } } } ); } macro_rules! generate_attribute_enum { ($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, $attr:tt, {$($(#[$enum_meta:meta])* $enum:ident => $enum_name:tt),+$(,)?}) => ( $(#[$meta])* #[derive(Debug, Clone, PartialEq)] pub enum $elem { $( $(#[$enum_meta])* $enum ),+ } impl ::core::convert::TryFrom for $elem { type Error = xso::error::FromElementError; fn try_from(elem: minidom::Element) -> Result<$elem, xso::error::FromElementError> { check_ns_only!(elem, $name, $ns); check_no_children!(elem, $name); check_no_unknown_attributes!(elem, $name, [$attr]); Ok(match get_attr!(elem, $attr, Required) { $($enum_name => $elem::$enum,)+ _ => return Err(xso::error::Error::Other(concat!("Invalid ", $name, " ", $attr, " value.")).into()), }) } } impl ::xso::FromXml for $elem { type Builder = ::xso::minidom_compat::FromEventsViaElement<$elem>; fn from_events( qname: ::xso::exports::rxml::QName, attrs: ::xso::exports::rxml::AttrMap, _ctx: &::xso::Context<'_>, ) -> Result { if qname.0 != crate::ns::$ns || qname.1 != $name { return Err(::xso::error::FromEventsError::Mismatch { name: qname, attrs, }) } Self::Builder::new(qname, attrs) } } impl From<$elem> for minidom::Element { fn from(elem: $elem) -> minidom::Element { minidom::Element::builder($name, crate::ns::$ns) .attr(::xso::exports::rxml::xml_ncname!($attr).into(), match elem { $($elem::$enum => $enum_name,)+ }) .build() } } impl ::xso::AsXml for $elem { type ItemIter<'x> = ::xso::minidom_compat::AsItemsViaElement<'x>; fn as_xml_iter(&self) -> Result, ::xso::error::Error> { ::xso::minidom_compat::AsItemsViaElement::new(self.clone()) } } ); } macro_rules! check_self { ($elem:ident, $name:tt, $ns:ident) => { check_self!($elem, $name, $ns, $name); }; ($elem:ident, $name:tt, $ns:ident, $pretty_name:tt) => { if !$elem.is($name, crate::ns::$ns) { return Err(xso::error::FromElementError::Mismatch($elem)); } }; } macro_rules! check_ns_only { ($elem:ident, $name:tt, $ns:ident) => { if !$elem.has_ns(crate::ns::$ns) { return Err(xso::error::Error::Other( concat!("This is not a ", $name, " element.").into(), ) .into()); } }; } macro_rules! check_no_children { ($elem:ident, $name:tt) => { #[cfg(not(feature = "disable-validation"))] for _ in $elem.children() { return Err(xso::error::Error::Other( concat!("Unknown child in ", $name, " element.").into(), ) .into()); } }; } macro_rules! check_no_attributes { ($elem:ident, $name:tt) => { #[cfg(not(feature = "disable-validation"))] for _ in $elem.attrs() { return Err(xso::error::Error::Other( concat!("Unknown attribute in ", $name, " element.").into(), ) .into()); } }; } macro_rules! check_no_unknown_attributes { ($elem:ident, $name:tt, [$($attr:tt),*]) => ( #[cfg(not(feature = "disable-validation"))] for ((ns, attr), _) in $elem.attrs() { $( if *ns == ::xso::exports::rxml::Namespace::NONE && attr == $attr { continue; } )* return Err(xso::error::Error::Other(concat!("Unknown attribute in ", $name, " element.")).into()); } ); } macro_rules! generate_id { ($(#[$meta:meta])* $elem:ident) => ( $(#[$meta])* #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct $elem(pub String); impl ::core::str::FromStr for $elem { type Err = xso::error::Error; fn from_str(s: &str) -> Result<$elem, xso::error::Error> { // TODO: add a way to parse that differently when needed. Ok($elem(String::from(s))) } } impl ::xso::FromXmlText for $elem { fn from_xml_text(s: String) -> Result<$elem, xso::error::Error> { Ok(Self(s)) } } impl ::xso::AsXmlText for $elem { fn as_xml_text(&self) ->Result<::alloc::borrow::Cow<'_, str>, xso::error::Error> { Ok(::alloc::borrow::Cow::Borrowed(self.0.as_str())) } } impl ::minidom::IntoAttributeValue for $elem { fn into_attribute_value(self) -> Option { Some(self.0) } } ); } macro_rules! generate_elem_id { ($(#[$meta:meta])* $elem:ident, $name:literal, $ns:ident) => ( generate_elem_id!($(#[$meta])* $elem, $name, $ns, String); impl ::core::str::FromStr for $elem { type Err = xso::error::Error; fn from_str(s: &str) -> Result<$elem, xso::error::Error> { // TODO: add a way to parse that differently when needed. Ok($elem(String::from(s))) } } ); ($(#[$meta:meta])* $elem:ident, $name:literal, $ns:ident, $type:ty) => ( $(#[$meta])* #[derive(xso::FromXml, xso::AsXml, Debug, Clone, PartialEq, Eq, Hash)] #[xml(namespace = crate::ns::$ns, name = $name)] pub struct $elem(#[xml(text)] pub $type); ); } #[cfg(test)] macro_rules! assert_size ( ($t:ty, $sz:expr) => ( assert_eq!(::core::mem::size_of::<$t>(), $sz); ); ); xmpp-parsers-0.22.0/src/util/mod.rs000064400000000000000000000005771046102023000152500ustar 00000000000000// Copyright (c) 2019 Emmanuel Gil Peyrot // // 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/. /// Helper macros to parse and serialise more easily. #[macro_use] mod macros; #[cfg(test)] mod macro_tests; xmpp-parsers-0.22.0/src/vcard.rs000064400000000000000000000117111046102023000146030ustar 00000000000000// 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/. //! This module implements vCard, for the purpose of vCard-based avatars as defined in //! [XEP-0054](https://xmpp.org/extensions/xep-0054.html). //! //! Only the `` element is supported as a member of this legacy vCard. For more modern and complete //! user profile management, see [XEP-0292](https://xmpp.org/extensions/xep-0292.html): vCard4 Over XMPP. //! //! For vCard updates defined in [XEP-0153](https://xmpp.org/extensions/xep-0153.html), //! see [`vcard_update`][crate::vcard_update] module. use xso::{ text::{Base64, StripWhitespace, TextCodec}, AsXml, FromXml, }; use crate::iq::{IqGetPayload, IqResultPayload, IqSetPayload}; use crate::ns; use minidom::Element; /// A photo element. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::VCARD, name = "PHOTO")] pub struct Photo { /// The type of the photo. #[xml(child)] pub type_: Type, /// The binary data of the photo. #[xml(child)] pub binval: Binval, } /// The type of the photo. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::VCARD, name = "TYPE")] pub struct Type { /// The type as a plain text string; at least "image/jpeg", "image/gif" and "image/png" SHOULD be supported. #[xml(text)] pub data: String, } /// The binary data of the photo. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::VCARD, name = "BINVAL")] pub struct Binval { /// The actual data. #[xml(text(codec = Base64.filtered(StripWhitespace)))] pub data: Vec, } /// A `` element; only the `` element is supported for this legacy vCard ; the rest is ignored. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::VCARD, name = "vCard")] pub struct VCard { /// A photo element. #[xml(child(default))] pub photo: Option, /// Every other element in the vCard. #[xml(element(n = ..))] pub payloads: Vec, } impl IqSetPayload for VCard {} impl IqResultPayload for VCard {} /// A `` request element, for use in iq get. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::VCARD, name = "vCard")] pub struct VCardQuery; impl IqGetPayload for VCardQuery {} #[cfg(test)] mod tests { use super::*; use core::str::FromStr; #[cfg(target_pointer_width = "32")] #[test] fn test_size() { assert_size!(Photo, 24); assert_size!(Type, 12); assert_size!(Binval, 12); assert_size!(VCard, 36); assert_size!(VCardQuery, 0); } #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(Photo, 48); assert_size!(Type, 24); assert_size!(Binval, 24); assert_size!(VCard, 72); assert_size!(VCardQuery, 0); } #[test] fn test_vcard() { // Create some bytes: let bytes = [0u8, 1, 2, 129]; // Test xml stolen from https://xmpp.org/extensions/xep-0153.html#example-5 let test_vcard = format!( r" 1476-06-09 Italy Verona JulietCapulet jcapulet@shakespeare.lit image/jpeg {} ", base64::Engine::encode(&base64::prelude::BASE64_STANDARD, &bytes) ); let test_vcard = Element::from_str(&test_vcard).expect("Failed to parse XML"); let test_vcard = VCard::try_from(test_vcard).expect("Failed to parse vCard"); let photo = test_vcard.photo.expect("No photo found"); assert_eq!(photo.type_.data, "image/jpeg".to_string()); assert_eq!(photo.binval.data, bytes); } #[test] fn test_vcard_with_linebreaks() { // Test xml stolen from https://xmpp.org/extensions/xep-0153.html#example-5 // extended to use a multi-line base64 string as is allowed as per RFC 2426 let test_vcard = r" 1476-06-09 Italy Verona JulietCapulet jcapulet@shakespeare.lit image/jpeg Zm9v Cg== "; let test_vcard = Element::from_str(&test_vcard).expect("Failed to parse XML"); let test_vcard = VCard::try_from(test_vcard).expect("Failed to parse vCard"); let photo = test_vcard.photo.expect("No photo found"); assert_eq!(photo.type_.data, "image/jpeg".to_string()); assert_eq!(photo.binval.data, b"foo\n"); } } xmpp-parsers-0.22.0/src/vcard_update.rs000064400000000000000000000053601046102023000161500ustar 00000000000000// 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/. //! This module implements vCard avatar updates defined in //! [XEP-0153](https://xmpp.org/extensions/xep-0153.html). //! //! Specifically, as it appears in `` stanzas, as shown in [XEP-0153 example 3](https://xmpp.org/extensions/xep-0153.html#example-3). //! //! For [XEP-0054](https://xmpp.org/extensions/xep-0054.html) vCard support, //! see [`vcard`][crate::vcard] module. use xso::{text::FixedHex, AsXml, FromXml}; use crate::ns; /// The presence payload for an avatar VCard update #[derive(FromXml, AsXml, Debug, PartialEq, Clone)] #[xml(namespace = ns::VCARD_UPDATE, name = "x")] pub struct VCardUpdate { /// The photo element. Is empty if "a client is not yet ready to advertise an image". #[xml(child(default))] pub photo: Option, } /// The photo element containing the avatar metadata #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::VCARD_UPDATE, name = "photo")] pub struct Photo { /// The SHA1 hash of the avatar. Empty when there is no photo. #[xml(text(codec = FixedHex<20>))] pub data: Option<[u8; 20]>, } #[cfg(test)] mod tests { use super::*; use core::str::FromStr; use minidom::Element; #[test] fn test_vcard_update() { // Test xml stolen from https://xmpp.org/extensions/xep-0153.html#example-3 // Changes: I did set the last d to uppercase to try to trip up a potentially case-sensitive parser. let test_vcard = r" 01b87fcd030b72895ff8e88db57ec525450f000D "; let test_vcard = Element::from_str(&test_vcard).expect("Failed to parse XML"); let test_vcard = VCardUpdate::try_from(test_vcard).expect("Failed to parse vCardUpdate"); let photo = test_vcard.photo.expect("No photo found"); assert_eq!( photo.data, Some([ 0x01, 0xb8, 0x7f, 0xcd, 0x03, 0x0b, 0x72, 0x89, 0x5f, 0xf8, 0xe8, 0x8d, 0xb5, 0x7e, 0xc5, 0x25, 0x45, 0x0f, 0x00, 0x0d ]) ); } #[test] fn test_vcard_update_empty() { // Test xml stolen from https://xmpp.org/extensions/xep-0153.html#example-7 let test_vcard = r""; let test_vcard = Element::from_str(&test_vcard).expect("Failed to parse XML"); let test_vcard = VCardUpdate::try_from(test_vcard).expect("Failed to parse vCardUpdate"); let photo = test_vcard.photo.expect("No photo found"); assert_eq!(photo.data, None) } } xmpp-parsers-0.22.0/src/version.rs000064400000000000000000000052331046102023000151730ustar 00000000000000// Copyright (c) 2017 Emmanuel Gil Peyrot // // 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 xso::{AsXml, FromXml}; use crate::iq::{IqGetPayload, IqResultPayload}; use crate::ns; /// Represents a query for the software version a remote entity is using. /// /// It should only be used in an ``, as it can only /// represent the request, and not a result. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::VERSION, name = "query")] pub struct VersionQuery; impl IqGetPayload for VersionQuery {} /// Represents the answer about the software version we are using. /// /// It should only be used in an ``, as it can only /// represent the result, and not a request. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::VERSION, name = "query")] pub struct VersionResult { /// The name of this client. #[xml(extract(fields(text)))] pub name: String, /// The version of this client. #[xml(extract(fields(text)))] pub version: String, /// The OS this client is running on. #[xml(extract(default, fields(text(type_ = String))))] pub os: Option, } impl IqResultPayload for VersionResult {} #[cfg(test)] mod tests { use super::*; use minidom::Element; #[cfg(target_pointer_width = "32")] #[test] fn test_size() { assert_size!(VersionQuery, 0); assert_size!(VersionResult, 36); } #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(VersionQuery, 0); assert_size!(VersionResult, 72); } #[test] fn simple() { let elem: Element = "xmpp-rs0.3.0" .parse() .unwrap(); let version = VersionResult::try_from(elem).unwrap(); assert_eq!(version.name, String::from("xmpp-rs")); assert_eq!(version.version, String::from("0.3.0")); assert_eq!(version.os, None); } #[test] fn serialisation() { let version = VersionResult { name: String::from("xmpp-rs"), version: String::from("0.3.0"), os: None, }; let elem1 = Element::from(version); let elem2: Element = "xmpp-rs0.3.0" .parse() .unwrap(); println!("{:?}", elem1); assert_eq!(elem1, elem2); } } xmpp-parsers-0.22.0/src/websocket.rs000064400000000000000000000057151046102023000155010ustar 00000000000000// Copyright (c) 2018 Emmanuel Gil Peyrot // // 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 xso::{AsXml, FromXml}; use jid::BareJid; use crate::ns; /// The stream opening for WebSocket. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::WEBSOCKET, name = "open")] pub struct Open { /// The JID of the entity opening this stream. #[xml(attribute(default))] pub from: Option, /// The JID of the entity receiving this stream opening. #[xml(attribute(default))] pub to: Option, /// The id of the stream, used for authentication challenges. #[xml(attribute(default))] pub id: Option, /// The XMPP version used during this stream. #[xml(attribute(default))] pub version: Option, /// The default human language for all subsequent stanzas, which will /// be transmitted to other entities for better localisation. #[xml(lang(default))] pub xml_lang: Option, } impl Open { /// Creates a simple client→server `` element. pub fn new(to: BareJid) -> Open { Open { from: None, to: Some(to), id: None, version: Some(String::from("1.0")), xml_lang: None, } } /// Sets the [@from](#structfield.from) attribute on this `` /// element. pub fn with_from(mut self, from: BareJid) -> Open { self.from = Some(from); self } /// Sets the [@id](#structfield.id) attribute on this `` element. pub fn with_id(mut self, id: String) -> Open { self.id = Some(id); self } /// Sets the [@xml:lang](#structfield.xml_lang) attribute on this `` /// element. pub fn with_lang(mut self, xml_lang: String) -> Open { self.xml_lang = Some(xml_lang); self } /// Checks whether the version matches the expected one. pub fn is_version(&self, version: &str) -> bool { match self.version { None => false, Some(ref self_version) => self_version == &String::from(version), } } } #[cfg(test)] mod tests { use super::*; use minidom::Element; #[cfg(target_pointer_width = "32")] #[test] fn test_size() { assert_size!(Open, 68); } #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(Open, 136); } #[test] fn test_simple() { let elem: Element = "" .parse() .unwrap(); let open = Open::try_from(elem).unwrap(); assert_eq!(open.from, None); assert_eq!(open.to, None); assert_eq!(open.id, None); assert_eq!(open.version, None); assert_eq!(open.xml_lang, None); } } xmpp-parsers-0.22.0/src/xhtml.rs000064400000000000000000000507621046102023000146510ustar 00000000000000// Copyright (c) 2019 Emmanuel Gil Peyrot // // 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 crate::message::MessagePayload; use crate::ns; use alloc::collections::BTreeMap; use minidom::{Element, Node}; use xso::exports::rxml; use xso::{ error::{Error, FromElementError}, exports::rxml::Namespace, }; // TODO: Use a proper lang type. type Lang = String; /// Container for formatted text. #[derive(Debug, Clone)] pub struct XhtmlIm { /// Map of language to body element. bodies: BTreeMap, } impl XhtmlIm { /// Serialise formatted text to HTML. pub fn into_html(self) -> String { let mut html = Vec::new(); // TODO: use the best language instead. // XXX: Remove this flag later when fixing the code below #[allow(clippy::never_loop)] for (lang, body) in self.bodies { if lang.is_empty() { assert!(body.xml_lang.is_none()); } else { assert_eq!(Some(lang), body.xml_lang); } for tag in body.children { html.push(tag.into_html()); } break; } html.concat() } /// Removes all unknown elements. fn flatten(self) -> XhtmlIm { let mut bodies = BTreeMap::new(); for (lang, body) in self.bodies { let children = body.children.into_iter().fold(vec![], |mut acc, child| { match child { Child::Tag(Tag::Unknown(children)) => acc.extend(children), any => acc.push(any), } acc }); let body = Body { children, ..body }; bodies.insert(lang, body); } XhtmlIm { bodies } } } impl MessagePayload for XhtmlIm {} impl TryFrom for XhtmlIm { type Error = FromElementError; fn try_from(elem: Element) -> Result { check_self!(elem, "html", XHTML_IM); check_no_attributes!(elem, "html"); let mut bodies = BTreeMap::new(); for child in elem.children() { if child.is("body", ns::XHTML) { let child = child.clone(); let lang = child .attr_ns(rxml::Namespace::xml(), "lang") .unwrap_or("") .to_string(); let body = Body::try_from(child)?; match bodies.insert(lang, body) { None => (), Some(_) => { return Err(Error::Other( "Two identical language bodies found in XHTML-IM.", ) .into()) } } } else { return Err(Error::Other("Unknown element in XHTML-IM.").into()); } } Ok(XhtmlIm { bodies }.flatten()) } } impl From for Element { fn from(wrapper: XhtmlIm) -> Element { Element::builder("html", ns::XHTML_IM) .append_all(wrapper.bodies.into_iter().map(|(lang, body)| { if lang.is_empty() { assert!(body.xml_lang.is_none()); } else { assert_eq!(Some(lang), body.xml_lang); } Element::from(body) })) .build() } } #[derive(Debug, Clone)] enum Child { Tag(Tag), Text(String), } impl Child { fn into_html(self) -> String { match self { Child::Tag(tag) => tag.into_html(), Child::Text(text) => text, } } } #[derive(Debug, Clone)] struct Property { key: String, value: String, } type Css = Vec; fn get_style_string(style: Css) -> Option { let mut result = vec![]; for Property { key, value } in style { result.push(format!("{}: {}", key, value)); } if result.is_empty() { return None; } Some(result.join("; ")) } #[derive(Debug, Clone)] struct Body { style: Css, xml_lang: Option, children: Vec, } impl TryFrom for Body { type Error = Error; fn try_from(elem: Element) -> Result { let mut children = vec![]; for child in elem.nodes() { match child { Node::Element(child) => children.push(Child::Tag(Tag::try_from(child.clone())?)), Node::Text(text) => children.push(Child::Text(text.clone())), } } Ok(Body { style: parse_css(elem.attr("style")), xml_lang: elem .attr_ns("xml", "lang") .map(|xml_lang| xml_lang.to_string()), children, }) } } impl From for Element { fn from(body: Body) -> Element { Element::builder("body", ns::XHTML) .attr( rxml::xml_ncname!("style").into(), get_style_string(body.style), ) .attr_ns( Into::::into(String::from("xml")), rxml::xml_ncname!("lang").into(), body.xml_lang, ) .append_all(children_to_nodes(body.children)) .build() } } #[derive(Debug, Clone)] enum Tag { A { href: Option, style: Css, type_: Option, children: Vec, }, Blockquote { style: Css, children: Vec, }, Br, Cite { style: Css, children: Vec, }, Em { children: Vec, }, Img { src: Option, alt: Option, }, // TODO: height, width, style Li { style: Css, children: Vec, }, Ol { style: Css, children: Vec, }, P { style: Css, children: Vec, }, Span { style: Css, children: Vec, }, Strong { children: Vec, }, Ul { style: Css, children: Vec, }, Unknown(Vec), } impl Tag { fn into_html(self) -> String { match self { Tag::A { href, style, type_, children, } => { let href = write_attr(href, "href"); let style = write_attr(get_style_string(style), "style"); let type_ = write_attr(type_, "type"); format!( "{}", href, style, type_, children_to_html(children) ) } Tag::Blockquote { style, children } => { let style = write_attr(get_style_string(style), "style"); format!( "{}", style, children_to_html(children) ) } Tag::Br => String::from("
"), Tag::Cite { style, children } => { let style = write_attr(get_style_string(style), "style"); format!("{}", style, children_to_html(children)) } Tag::Em { children } => format!("{}", children_to_html(children)), Tag::Img { src, alt } => { let src = write_attr(src, "src"); let alt = write_attr(alt, "alt"); format!("", src, alt) } Tag::Li { style, children } => { let style = write_attr(get_style_string(style), "style"); format!("{}", style, children_to_html(children)) } Tag::Ol { style, children } => { let style = write_attr(get_style_string(style), "style"); format!("{}", style, children_to_html(children)) } Tag::P { style, children } => { let style = write_attr(get_style_string(style), "style"); format!("{}

", style, children_to_html(children)) } Tag::Span { style, children } => { let style = write_attr(get_style_string(style), "style"); format!("{}", style, children_to_html(children)) } Tag::Strong { children } => format!("{}", children_to_html(children)), Tag::Ul { style, children } => { let style = write_attr(get_style_string(style), "style"); format!("{}", style, children_to_html(children)) } Tag::Unknown(_) => { panic!("No unknown element should be present in XHTML-IM after parsing.") } } } } impl TryFrom for Tag { type Error = Error; fn try_from(elem: Element) -> Result { let mut children = vec![]; for child in elem.nodes() { match child { Node::Element(child) => children.push(Child::Tag(Tag::try_from(child.clone())?)), Node::Text(text) => children.push(Child::Text(text.clone())), } } Ok(match elem.name() { "a" => Tag::A { href: elem.attr("href").map(|href| href.to_string()), style: parse_css(elem.attr(rxml::xml_ncname!("style"))), type_: elem.attr("type").map(|type_| type_.to_string()), children, }, "blockquote" => Tag::Blockquote { style: parse_css(elem.attr("style")), children, }, "br" => Tag::Br, "cite" => Tag::Cite { style: parse_css(elem.attr("style")), children, }, "em" => Tag::Em { children }, "img" => Tag::Img { src: elem.attr("src").map(|src| src.to_string()), alt: elem.attr("alt").map(|alt| alt.to_string()), }, "li" => Tag::Li { style: parse_css(elem.attr("style")), children, }, "ol" => Tag::Ol { style: parse_css(elem.attr("style")), children, }, "p" => Tag::P { style: parse_css(elem.attr("style")), children, }, "span" => Tag::Span { style: parse_css(elem.attr("style")), children, }, "strong" => Tag::Strong { children }, "ul" => Tag::Ul { style: parse_css(elem.attr("style")), children, }, _ => Tag::Unknown(children), }) } } impl From for Element { fn from(tag: Tag) -> Element { let (name, attrs, children) = match tag { Tag::A { href, style, type_, children, } => ( "a", { let mut attrs = vec![]; if let Some(href) = href { attrs.push((rxml::xml_ncname!("href"), href)); } if let Some(style) = get_style_string(style) { attrs.push((rxml::xml_ncname!("style"), style)); } if let Some(type_) = type_ { attrs.push((rxml::xml_ncname!("type"), type_)); } attrs }, children, ), Tag::Blockquote { style, children } => ( "blockquote", match get_style_string(style) { Some(style) => vec![(rxml::xml_ncname!("style"), style)], None => vec![], }, children, ), Tag::Br => ("br", vec![], vec![]), Tag::Cite { style, children } => ( "cite", match get_style_string(style) { Some(style) => vec![(rxml::xml_ncname!("style"), style)], None => vec![], }, children, ), Tag::Em { children } => ("em", vec![], children), Tag::Img { src, alt } => { let mut attrs = vec![]; if let Some(src) = src { attrs.push((rxml::xml_ncname!("src"), src)); } if let Some(alt) = alt { attrs.push((rxml::xml_ncname!("alt"), alt)); } ("img", attrs, vec![]) } Tag::Li { style, children } => ( "li", match get_style_string(style) { Some(style) => vec![(rxml::xml_ncname!("style"), style)], None => vec![], }, children, ), Tag::Ol { style, children } => ( "ol", match get_style_string(style) { Some(style) => vec![(rxml::xml_ncname!("style"), style)], None => vec![], }, children, ), Tag::P { style, children } => ( "p", match get_style_string(style) { Some(style) => vec![(rxml::xml_ncname!("style"), style)], None => vec![], }, children, ), Tag::Span { style, children } => ( "span", match get_style_string(style) { Some(style) => vec![(rxml::xml_ncname!("style"), style)], None => vec![], }, children, ), Tag::Strong { children } => ("strong", vec![], children), Tag::Ul { style, children } => ( "ul", match get_style_string(style) { Some(style) => vec![(rxml::xml_ncname!("style"), style)], None => vec![], }, children, ), Tag::Unknown(_) => { panic!("No unknown element should be present in XHTML-IM after parsing.") } }; let mut builder = Element::builder(name, ns::XHTML).append_all(children_to_nodes(children)); for (key, value) in attrs { builder = builder.attr(key.into(), value); } builder.build() } } fn children_to_nodes(children: Vec) -> impl IntoIterator { children.into_iter().map(|child| match child { Child::Tag(tag) => Node::Element(Element::from(tag)), Child::Text(text) => Node::Text(text), }) } fn children_to_html(children: Vec) -> String { children .into_iter() .map(|child| child.into_html()) .collect::>() .concat() } fn write_attr(attr: Option, name: &str) -> String { match attr { Some(attr) => format!(" {}='{}'", name, attr), None => String::new(), } } fn parse_css(style: Option<&str>) -> Css { let mut properties = vec![]; if let Some(style) = style { // TODO: make that parser a bit more resilient to things. for part in style.split(';') { let mut part = part .splitn(2, ':') .map(|a| a.to_string()) .collect::>(); let key = part.pop().unwrap(); let value = part.pop().unwrap(); properties.push(Property { key, value }); } } properties } #[cfg(test)] mod tests { use super::*; #[cfg(target_pointer_width = "32")] #[test] fn test_size() { assert_size!(XhtmlIm, 12); assert_size!(Child, 48); assert_size!(Tag, 48); } #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(XhtmlIm, 24); assert_size!(Child, 96); assert_size!(Tag, 96); } #[test] fn test_empty() { let elem: Element = "" .parse() .unwrap(); let xhtml = XhtmlIm::try_from(elem).unwrap(); assert_eq!(xhtml.bodies.len(), 0); let elem: Element = "" .parse() .unwrap(); let xhtml = XhtmlIm::try_from(elem).unwrap(); assert_eq!(xhtml.bodies.len(), 1); let elem: Element = "" .parse() .unwrap(); let xhtml = XhtmlIm::try_from(elem).unwrap(); assert_eq!(xhtml.bodies.len(), 2); } #[test] fn invalid_two_same_langs() { let elem: Element = "" .parse() .unwrap(); let error = XhtmlIm::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!(message, "Two identical language bodies found in XHTML-IM."); } #[test] fn test_tag() { let elem: Element = "" .parse() .unwrap(); let body = Body::try_from(elem).unwrap(); assert_eq!(body.children.len(), 0); let elem: Element = "

Hello world!

" .parse() .unwrap(); let mut body = Body::try_from(elem).unwrap(); assert_eq!(body.style.len(), 0); assert_eq!(body.xml_lang, None); assert_eq!(body.children.len(), 1); let p = match body.children.pop() { Some(Child::Tag(tag)) => tag, _ => panic!(), }; let mut children = match p { Tag::P { style, children } => { assert_eq!(style.len(), 0); assert_eq!(children.len(), 1); children } _ => panic!(), }; let text = match children.pop() { Some(Child::Text(text)) => text, _ => panic!(), }; assert_eq!(text, "Hello world!"); } #[test] fn test_unknown_element() { let elem: Element = "Hello world!" .parse() .unwrap(); let parsed = XhtmlIm::try_from(elem).unwrap(); let parsed2 = parsed.clone(); let html = parsed.into_html(); assert_eq!(html, "Hello world!"); let elem = Element::from(parsed2); assert_eq!(String::from(&elem), "Hello world!"); } #[test] fn test_generate_html() { let elem: Element = "

Hello world!

" .parse() .unwrap(); let xhtml_im = XhtmlIm::try_from(elem).unwrap(); let html = xhtml_im.into_html(); assert_eq!(html, "

Hello world!

"); let elem: Element = "

Hello world!

" .parse() .unwrap(); let xhtml_im = XhtmlIm::try_from(elem).unwrap(); let html = xhtml_im.into_html(); assert_eq!(html, "

Hello world!

"); } #[test] fn generate_tree() { let world = "world".to_string(); Body { style: vec![], xml_lang: Some("en".to_string()), children: vec![Child::Tag(Tag::P { style: vec![], children: vec![ Child::Text("Hello ".to_string()), Child::Tag(Tag::Strong { children: vec![Child::Text(world)], }), Child::Text("!".to_string()), ], })], }; } }