Implement Bits of Binary.

Emmanuel Gil Peyrot created

Change summary

ChangeLog  |   1 
doap.xml   |   8 ++
src/bob.rs | 168 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/lib.rs |   3 +
src/ns.rs  |   3 +
5 files changed, 183 insertions(+)

Detailed changes

ChangeLog 🔗

@@ -2,6 +2,7 @@ Version NEXT:
 DATE  Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
     * New parsers/serialisers:
         - Message Carbons (XEP-0280)
+        - Bits of Binary (XEP-0231)
     * Breaking changes:
         - Stop reexporting TryFrom and TryInto, they are available in
           std::convert nowadays.

doap.xml 🔗

@@ -279,6 +279,14 @@
             <xmpp:since>0.1.0</xmpp:since>
         </xmpp:SupportedXep>
     </implements>
+    <implements>
+        <xmpp:SupportedXep>
+            <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0231.html"/>
+            <xmpp:status>complete</xmpp:status>
+            <xmpp:version>1.0</xmpp:version>
+            <xmpp:since>NEXT</xmpp:since>
+        </xmpp:SupportedXep>
+    </implements>
     <implements>
         <xmpp:SupportedXep>
             <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0234.html"/>

src/bob.rs 🔗

@@ -0,0 +1,168 @@
+// Copyright (c) 2019 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
+//
+// 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::hashes::{Hash, Algo};
+use crate::util::helpers::Base64;
+use crate::util::error::Error;
+use minidom::IntoAttributeValue;
+use std::str::FromStr;
+
+/// 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)]
+pub struct ContentId {
+    hash: Hash,
+}
+
+impl FromStr for ContentId {
+    type Err = Error;
+
+    fn from_str(s: &str) -> Result<Self, Error> {
+        let temp: Vec<_> = s.splitn(2, '@').collect();
+        let temp: Vec<_> = match temp[..] {
+            [lhs, rhs] => {
+                if rhs != "bob.xmpp.org" {
+                    return Err(Error::ParseError("Wrong domain for cid URI."))
+                }
+                lhs.splitn(2, '+').collect()
+            },
+            _ => return Err(Error::ParseError("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::ParseError("Missing + in cid URI."))
+        };
+        let hash = Hash::from_hex(algo, hex)?;
+        Ok(ContentId { hash })
+    }
+}
+
+impl IntoAttributeValue for ContentId {
+    fn into_attribute_value(self) -> Option<String> {
+        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()))
+    }
+}
+
+generate_element!(
+    /// Request for an uncached cid file.
+    Data, "data", BOB,
+    attributes: [
+        /// The cid in question.
+        cid: Required<ContentId> = "cid",
+
+        /// How long to cache it (in seconds).
+        max_age: Option<usize> = "max-age",
+
+        /// 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]: https://www.iana.org/assignments/media-types/media-types.xhtml
+        type_: Option<String> = "type"
+    ],
+    text: (
+        /// The actual data.
+        data: Base64<Vec<u8>>
+    )
+);
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use minidom::Element;
+    use std::convert::TryFrom;
+    use std::error::Error as StdError;
+
+    #[cfg(target_pointer_width = "32")]
+    #[test]
+    fn test_size() {
+        assert_size!(ContentId, 24);
+        assert_size!(Data, 24);
+    }
+
+    #[cfg(target_pointer_width = "64")]
+    #[test]
+    fn test_size() {
+        assert_size!(ContentId, 56);
+        assert_size!(Data, 120);
+    }
+
+    #[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 = "<data xmlns='urn:xmpp:bob' cid='sha1+8f35fef110ffc5df08d579a50083ff9308fb6242@bob.xmpp.org'/>".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::<ContentId>().unwrap_err();
+        let message = match error {
+            Error::ParseError(string) => string,
+            _ => panic!(),
+        };
+        assert_eq!(message, "Missing @ in cid URI.");
+
+        let error = "Hello world@bob.xmpp.org".parse::<ContentId>().unwrap_err();
+        let message = match error {
+            Error::ParseError(string) => string,
+            _ => panic!(),
+        };
+        assert_eq!(message, "Missing + in cid URI.");
+
+        let error = "sha1+1234@coucou.linkmauve.fr".parse::<ContentId>().unwrap_err();
+        let message = match error {
+            Error::ParseError(string) => string,
+            _ => panic!(),
+        };
+        assert_eq!(message, "Wrong domain for cid URI.");
+
+        let error = "sha1+invalid@bob.xmpp.org".parse::<ContentId>().unwrap_err();
+        let message = match error {
+            Error::ParseIntError(error) => error,
+            _ => panic!(),
+        };
+        assert_eq!(message.description(), "invalid digit found in string");
+    }
+
+    #[test]
+    fn unknown_child() {
+        let elem: Element = "<data xmlns='urn:xmpp:bob'><coucou/></data>"
+            .parse()
+            .unwrap();
+        let error = Data::try_from(elem).unwrap_err();
+        let message = match error {
+            Error::ParseError(string) => string,
+            _ => panic!(),
+        };
+        assert_eq!(message, "Unknown child in data element.");
+    }
+}

src/lib.rs 🔗

@@ -138,6 +138,9 @@ 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;
 

src/ns.rs 🔗

@@ -124,6 +124,9 @@ 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