ur/
ur.rs

1//! Split up big payloads into constantly sized URIs which can be recombined by a decoder.
2//!
3//! The `ur` module provides thin wrappers around fountain en- and decoders
4//! which turn these fountain parts into URIs. To this end the fountain part
5//! attributes (data, checksum, indexes being used, etc.) are combined with
6//! CBOR into a self-describing byte payload and encoded with the `bytewords`
7//! encoding into URIs suitable for web transport and QR codes.
8//! ```
9//! let data = String::from("Ten chars!").repeat(10);
10//! let max_length = 5;
11//! let mut encoder = ur::Encoder::bytes(data.as_bytes(), max_length).unwrap();
12//! let part = encoder.next_part().unwrap();
13//! assert_eq!(
14//!     part,
15//!     "ur:bytes/1-20/lpadbbcsiecyvdidatkpfeghihjtcxiabdfevlms"
16//! );
17//! let mut decoder = ur::Decoder::default();
18//! while !decoder.complete() {
19//!     let part = encoder.next_part().unwrap();
20//!     // Simulate some communication loss
21//!     if encoder.current_index() & 1 > 0 {
22//!         decoder.receive(&part).unwrap();
23//!     }
24//! }
25//! assert_eq!(decoder.message().unwrap().as_deref(), Some(data.as_bytes()));
26//! ```
27
28extern crate alloc;
29use alloc::{string::String, vec::Vec};
30
31/// Errors that can happen during encoding and decoding of URs.
32#[derive(Debug)]
33pub enum Error {
34    /// A bytewords error.
35    Bytewords(crate::bytewords::Error),
36    /// A fountain error.
37    Fountain(crate::fountain::Error),
38    /// Invalid scheme.
39    InvalidScheme,
40    /// No type specified.
41    TypeUnspecified,
42    /// Invalid characters.
43    InvalidCharacters,
44    /// Invalid indices in multi-part UR.
45    InvalidIndices,
46    /// Tried to decode a single-part UR as multi-part.
47    NotMultiPart,
48}
49
50impl core::fmt::Display for Error {
51    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
52        match self {
53            Self::Bytewords(e) => write!(f, "{e}"),
54            Self::Fountain(e) => write!(f, "{e}"),
55            Self::InvalidScheme => write!(f, "Invalid scheme"),
56            Self::TypeUnspecified => write!(f, "No type specified"),
57            Self::InvalidCharacters => write!(f, "Type contains invalid characters"),
58            Self::InvalidIndices => write!(f, "Invalid indices"),
59            Self::NotMultiPart => write!(f, "Can't decode single-part UR as multi-part"),
60        }
61    }
62}
63
64impl From<crate::bytewords::Error> for Error {
65    fn from(e: crate::bytewords::Error) -> Self {
66        Self::Bytewords(e)
67    }
68}
69
70impl From<crate::fountain::Error> for Error {
71    fn from(e: crate::fountain::Error) -> Self {
72        Self::Fountain(e)
73    }
74}
75
76/// Encodes a data payload into a single URI
77///
78/// # Examples
79///
80/// ```
81/// assert_eq!(
82///     ur::ur::encode(b"data", &ur::Type::Bytes),
83///     "ur:bytes/iehsjyhspmwfwfia"
84/// );
85/// ```
86#[must_use]
87pub fn encode(data: &[u8], ur_type: &Type) -> String {
88    let body = crate::bytewords::encode(data, crate::bytewords::Style::Minimal);
89    alloc::format!("ur:{}/{body}", ur_type.encoding())
90}
91
92/// The type of uniform resource.
93pub enum Type<'a> {
94    /// A `bytes` uniform resource.
95    Bytes,
96    /// A custom uniform resource.
97    Custom(&'a str),
98}
99
100impl<'a> Type<'a> {
101    const fn encoding(&self) -> &'a str {
102        match self {
103            Self::Bytes => "bytes",
104            Self::Custom(s) => s,
105        }
106    }
107}
108
109/// A uniform resource encoder with an underlying fountain encoding.
110///
111/// # Examples
112///
113/// See the [`crate::ur`] module documentation for an example.
114pub struct Encoder<'a> {
115    fountain: crate::fountain::Encoder,
116    ur_type: Type<'a>,
117}
118
119impl<'a> Encoder<'a> {
120    /// Creates a new [`bytes`] [`Encoder`] for given a message payload.
121    ///
122    /// The emitted fountain parts will respect the maximum fragment length argument.
123    ///
124    /// # Examples
125    ///
126    /// See the [`crate::ur`] module documentation for an example.
127    ///
128    /// # Errors
129    ///
130    /// If an empty message or a zero maximum fragment length is passed, an error
131    /// will be returned.
132    ///
133    /// [`bytes`]: Type::Bytes
134    pub fn bytes(message: &[u8], max_fragment_length: usize) -> Result<Self, Error> {
135        Ok(Self {
136            fountain: crate::fountain::Encoder::new(message, max_fragment_length)?,
137            ur_type: Type::Bytes,
138        })
139    }
140
141    /// Creates a new [`custom`] [`Encoder`] for given a message payload.
142    ///
143    /// The emitted fountain parts will respect the maximum fragment length argument.
144    ///
145    /// # Errors
146    ///
147    /// If an empty message or a zero maximum fragment length is passed, an error
148    /// will be returned.
149    ///
150    /// [`custom`]: Type::Custom
151    pub fn new(message: &[u8], max_fragment_length: usize, s: &'a str) -> Result<Self, Error> {
152        Ok(Self {
153            fountain: crate::fountain::Encoder::new(message, max_fragment_length)?,
154            ur_type: Type::Custom(s),
155        })
156    }
157
158    /// Returns the URI corresponding to next fountain part.
159    ///
160    /// # Examples
161    ///
162    /// See the [`crate::ur`] module documentation for an example.
163    ///
164    /// # Errors
165    ///
166    /// If serialization fails an error will be returned.
167    pub fn next_part(&mut self) -> Result<String, Error> {
168        let part = self.fountain.next_part();
169        let body = crate::bytewords::encode(&part.cbor()?, crate::bytewords::Style::Minimal);
170        Ok(alloc::format!(
171            "ur:{}/{}/{body}",
172            self.ur_type.encoding(),
173            part.sequence_id()
174        ))
175    }
176
177    /// Returns the current count of already emitted parts.
178    ///
179    /// # Examples
180    ///
181    /// ```
182    /// let mut encoder = ur::Encoder::bytes(b"data", 5).unwrap();
183    /// assert_eq!(encoder.current_index(), 0);
184    /// encoder.next_part().unwrap();
185    /// assert_eq!(encoder.current_index(), 1);
186    /// ```
187    #[must_use]
188    pub const fn current_index(&self) -> usize {
189        self.fountain.current_sequence()
190    }
191
192    /// Returns the number of segments the original message has been split up into.
193    ///
194    /// # Examples
195    ///
196    /// ```
197    /// let mut encoder = ur::Encoder::bytes(b"data", 3).unwrap();
198    /// assert_eq!(encoder.fragment_count(), 2);
199    /// ```
200    #[must_use]
201    pub fn fragment_count(&self) -> usize {
202        self.fountain.fragment_count()
203    }
204}
205
206/// An enum used to indicate whether a UR is single- or
207/// multip-part. See e.g. [`decode`] where it is returned.
208#[derive(Debug, PartialEq, Eq)]
209pub enum Kind {
210    /// This UR contains the full data payload.
211    SinglePart,
212    /// This UR contains part of the data payload.
213    MultiPart,
214}
215
216/// Decodes a single URI (either single- or multi-part)
217/// into a tuple consisting of the [`Kind`] and the data
218/// payload.
219///
220/// # Examples
221///
222/// ```
223/// assert_eq!(
224///     ur::ur::decode("ur:bytes/iehsjyhspmwfwfia").unwrap(),
225///     (ur::ur::Kind::SinglePart, b"data".to_vec())
226/// );
227/// assert_eq!(
228///     ur::ur::decode("ur:bytes/1-2/iehsjyhspmwfwfia").unwrap(),
229///     (ur::ur::Kind::MultiPart, b"data".to_vec())
230/// );
231/// ```
232///
233/// # Errors
234///
235/// This function errors for invalid inputs, for example
236/// an invalid scheme different from "ur" or an invalid number
237/// of "/" separators.
238pub fn decode(value: &str) -> Result<(Kind, Vec<u8>), Error> {
239    let strip_scheme = value.strip_prefix("ur:").ok_or(Error::InvalidScheme)?;
240    let (r#type, strip_type) = strip_scheme.split_once('/').ok_or(Error::TypeUnspecified)?;
241
242    if !r#type
243        .trim_start_matches(|c: char| c.is_ascii_alphanumeric() || c == '-')
244        .is_empty()
245    {
246        return Err(Error::InvalidCharacters);
247    }
248
249    match strip_type.rsplit_once('/') {
250        None => Ok((
251            Kind::SinglePart,
252            crate::bytewords::decode(strip_type, crate::bytewords::Style::Minimal)?,
253        )),
254        Some((indices, payload)) => {
255            let (idx, idx_total) = indices.split_once('-').ok_or(Error::InvalidIndices)?;
256            if idx.parse::<u16>().is_err() || idx_total.parse::<u16>().is_err() {
257                return Err(Error::InvalidIndices);
258            }
259
260            Ok((
261                Kind::MultiPart,
262                crate::bytewords::decode(payload, crate::bytewords::Style::Minimal)?,
263            ))
264        }
265    }
266}
267
268/// A uniform resource decoder able to receive URIs that encode a fountain part.
269///
270/// # Examples
271///
272/// See the [`crate::ur`] module documentation for an example.
273#[derive(Default)]
274pub struct Decoder {
275    fountain: crate::fountain::Decoder,
276}
277
278impl Decoder {
279    /// Receives a URI representing a CBOR and `bytewords`-encoded fountain part
280    /// into the decoder.
281    ///
282    /// # Examples
283    ///
284    /// See the [`crate::ur`] module documentation for an example.
285    ///
286    /// # Errors
287    ///
288    /// This function may error along all the necessary decoding steps:
289    ///  - The string may not be a well-formed URI according to the uniform resource scheme
290    ///  - The URI payload may not be a well-formed `bytewords` string
291    ///  - The decoded byte payload may not be valid CBOR
292    ///  - The CBOR-encoded fountain part may be inconsistent with previously received ones
293    ///
294    /// In all these cases, an error will be returned.
295    pub fn receive(&mut self, value: &str) -> Result<(), Error> {
296        let (kind, decoded) = decode(value)?;
297        if kind != Kind::MultiPart {
298            return Err(Error::NotMultiPart);
299        }
300
301        self.fountain
302            .receive(crate::fountain::Part::from_cbor(decoded.as_slice())?)?;
303        Ok(())
304    }
305
306    /// Returns whether the decoder is complete and hence the message available.
307    ///
308    /// # Examples
309    ///
310    /// See the [`crate::ur`] module documentation for an example.
311    #[must_use]
312    pub fn complete(&self) -> bool {
313        self.fountain.complete()
314    }
315
316    /// If [`complete`], returns the decoded message, `None` otherwise.
317    ///
318    /// # Errors
319    ///
320    /// If an inconsistent internal state detected, an error will be returned.
321    ///
322    /// # Examples
323    ///
324    /// See the [`crate::ur`] module documentation for an example.
325    ///
326    /// [`complete`]: Decoder::complete
327    pub fn message(&self) -> Result<Option<Vec<u8>>, Error> {
328        self.fountain.message().map_err(Error::from)
329    }
330}
331
332#[cfg(test)]
333mod tests {
334    use super::*;
335    use minicbor::{bytes::ByteVec, data::Tag};
336
337    fn make_message_ur(length: usize, seed: &str) -> Vec<u8> {
338        let message = crate::xoshiro::test_utils::make_message(seed, length);
339        minicbor::to_vec(ByteVec::from(message)).unwrap()
340    }
341
342    #[test]
343    fn test_single_part_ur() {
344        let ur = make_message_ur(50, "Wolf");
345        let encoded = encode(&ur, &Type::Bytes);
346        let expected = "ur:bytes/hdeymejtswhhylkepmykhhtsytsnoyoyaxaedsuttydmmhhpktpmsrjtgwdpfnsboxgwlbaawzuefywkdplrsrjynbvygabwjldapfcsdwkbrkch";
347        assert_eq!(encoded, expected);
348        let decoded = decode(&encoded).unwrap();
349        assert_eq!((Kind::SinglePart, ur), decoded);
350    }
351
352    #[test]
353    fn test_ur_encoder() {
354        let ur = make_message_ur(256, "Wolf");
355        let mut encoder = Encoder::bytes(&ur, 30).unwrap();
356        let expected = vec![
357            "ur:bytes/1-9/lpadascfadaxcywenbpljkhdcahkadaemejtswhhylkepmykhhtsytsnoyoyaxaedsuttydmmhhpktpmsrjtdkgslpgh",
358            "ur:bytes/2-9/lpaoascfadaxcywenbpljkhdcagwdpfnsboxgwlbaawzuefywkdplrsrjynbvygabwjldapfcsgmghhkhstlrdcxaefz",
359            "ur:bytes/3-9/lpaxascfadaxcywenbpljkhdcahelbknlkuejnbadmssfhfrdpsbiegecpasvssovlgeykssjykklronvsjksopdzmol",
360            "ur:bytes/4-9/lpaaascfadaxcywenbpljkhdcasotkhemthydawydtaxneurlkosgwcekonertkbrlwmplssjtammdplolsbrdzcrtas",
361            "ur:bytes/5-9/lpahascfadaxcywenbpljkhdcatbbdfmssrkzmcwnezelennjpfzbgmuktrhtejscktelgfpdlrkfyfwdajldejokbwf",
362            "ur:bytes/6-9/lpamascfadaxcywenbpljkhdcackjlhkhybssklbwefectpfnbbectrljectpavyrolkzczcpkmwidmwoxkilghdsowp",
363            "ur:bytes/7-9/lpatascfadaxcywenbpljkhdcavszmwnjkwtclrtvaynhpahrtoxmwvwatmedibkaegdosftvandiodagdhthtrlnnhy",
364            "ur:bytes/8-9/lpayascfadaxcywenbpljkhdcadmsponkkbbhgsoltjntegepmttmoonftnbuoiyrehfrtsabzsttorodklubbuyaetk",
365            "ur:bytes/9-9/lpasascfadaxcywenbpljkhdcajskecpmdckihdyhphfotjojtfmlnwmadspaxrkytbztpbauotbgtgtaeaevtgavtny",
366            "ur:bytes/10-9/lpbkascfadaxcywenbpljkhdcahkadaemejtswhhylkepmykhhtsytsnoyoyaxaedsuttydmmhhpktpmsrjtwdkiplzs",
367            "ur:bytes/11-9/lpbdascfadaxcywenbpljkhdcahelbknlkuejnbadmssfhfrdpsbiegecpasvssovlgeykssjykklronvsjkvetiiapk",
368            "ur:bytes/12-9/lpbnascfadaxcywenbpljkhdcarllaluzmdmgstospeyiefmwejlwtpedamktksrvlcygmzemovovllarodtmtbnptrs",
369            "ur:bytes/13-9/lpbtascfadaxcywenbpljkhdcamtkgtpknghchchyketwsvwgwfdhpgmgtylctotzopdrpayoschcmhplffziachrfgd",
370            "ur:bytes/14-9/lpbaascfadaxcywenbpljkhdcapazewnvonnvdnsbyleynwtnsjkjndeoldydkbkdslgjkbbkortbelomueekgvstegt",
371            "ur:bytes/15-9/lpbsascfadaxcywenbpljkhdcaynmhpddpzmversbdqdfyrehnqzlugmjzmnmtwmrouohtstgsbsahpawkditkckynwt",
372            "ur:bytes/16-9/lpbeascfadaxcywenbpljkhdcawygekobamwtlihsnpalnsghenskkiynthdzotsimtojetprsttmukirlrsbtamjtpd",
373            "ur:bytes/17-9/lpbyascfadaxcywenbpljkhdcamklgftaxykpewyrtqzhydntpnytyisincxmhtbceaykolduortotiaiaiafhiaoyce",
374            "ur:bytes/18-9/lpbgascfadaxcywenbpljkhdcahkadaemejtswhhylkepmykhhtsytsnoyoyaxaedsuttydmmhhpktpmsrjtntwkbkwy",
375            "ur:bytes/19-9/lpbwascfadaxcywenbpljkhdcadekicpaajootjzpsdrbalpeywllbdsnbinaerkurspbncxgslgftvtsrjtksplcpeo",
376            "ur:bytes/20-9/lpbbascfadaxcywenbpljkhdcayapmrleeleaxpasfrtrdkncffwjyjzgyetdmlewtkpktgllepfrltataztksmhkbot",
377        ];
378        assert_eq!(encoder.fragment_count(), 9);
379        for (index, e) in expected.into_iter().enumerate() {
380            assert_eq!(encoder.current_index(), index);
381            assert_eq!(encoder.next_part().unwrap(), e);
382        }
383    }
384
385    #[test]
386    fn test_ur_encoder_decoder_bc_crypto_request() {
387        // https://github.com/BlockchainCommons/crypto-commons/blob/67ea252f4a7f295bb347cb046796d5b445b3ad3c/Docs/ur-99-request-response.md#the-seed-request
388
389        fn crypto_seed() -> Result<Vec<u8>, minicbor::encode::Error<std::convert::Infallible>> {
390            let mut e = minicbor::Encoder::new(Vec::new());
391
392            let uuid = hex::decode("020C223A86F7464693FC650EF3CAC047").unwrap();
393            let seed_digest =
394                hex::decode("E824467CAFFEAF3BBC3E0CA095E660A9BAD80DDB6A919433A37161908B9A3986")
395                    .unwrap();
396
397            #[rustfmt::skip]
398            e.map(2)?
399                // 2.1 UUID: tag 37 type bytes(16)
400                .u8(1)?.tag(Tag::Unassigned(37))?.bytes(&uuid)?
401                // 2.2 crypto-seed: tag 500 type map
402                .u8(2)?.tag(Tag::Unassigned(500))?.map(1)?
403                // 2.2.1 crypto-seed-digest: tag 600 type bytes(32)
404                .u8(1)?.tag(Tag::Unassigned(600))?.bytes(&seed_digest)?;
405
406            Ok(e.into_writer())
407        }
408
409        let data = crypto_seed().unwrap();
410
411        let e = encode(&data, &Type::Custom("crypto-request"));
412        let expected = "ur:crypto-request/oeadtpdagdaobncpftlnylfgfgmuztihbawfsgrtflaotaadwkoyadtaaohdhdcxvsdkfgkepezepefrrffmbnnbmdvahnptrdtpbtuyimmemweootjshsmhlunyeslnameyhsdi";
413        assert_eq!(expected, e);
414
415        // Decoding should yield the same data
416        let decoded = decode(e.as_str()).unwrap();
417        assert_eq!((Kind::SinglePart, data), decoded);
418    }
419
420    #[test]
421    fn test_multipart_ur() {
422        let ur = make_message_ur(32767, "Wolf");
423        let mut encoder = Encoder::bytes(&ur, 1000).unwrap();
424        let mut decoder = Decoder::default();
425        while !decoder.complete() {
426            assert_eq!(decoder.message().unwrap(), None);
427            decoder.receive(&encoder.next_part().unwrap()).unwrap();
428        }
429        assert_eq!(decoder.message().unwrap(), Some(ur));
430    }
431
432    #[test]
433    fn test_decoder() {
434        assert!(matches!(
435            decode("uhr:bytes/aeadaolazmjendeoti"),
436            Err(Error::InvalidScheme)
437        ));
438        assert!(matches!(
439            decode("ur:aeadaolazmjendeoti"),
440            Err(Error::TypeUnspecified)
441        ));
442        assert!(matches!(
443            decode("ur:bytes#4/aeadaolazmjendeoti"),
444            Err(Error::InvalidCharacters)
445        ));
446        assert!(matches!(
447            decode("ur:bytes/1-1a/aeadaolazmjendeoti"),
448            Err(Error::InvalidIndices)
449        ));
450        assert!(matches!(
451            decode("ur:bytes/1-1/toomuch/aeadaolazmjendeoti"),
452            Err(Error::InvalidIndices)
453        ));
454        decode("ur:bytes/aeadaolazmjendeoti").unwrap();
455        decode("ur:whatever-12/aeadaolazmjendeoti").unwrap();
456    }
457
458    #[test]
459    fn test_custom_encoder() {
460        let data = String::from("Ten chars!");
461        let max_length = 5;
462        let mut encoder = Encoder::new(data.as_bytes(), max_length, "my-scheme").unwrap();
463        assert_eq!(
464            encoder.next_part().unwrap(),
465            "ur:my-scheme/1-2/lpadaobkcywkwmhfwnfeghihjtcxiansvomopr"
466        );
467    }
468}