urbit_q/
lib.rs

1//! # urbit-q
2//!
3//! Based on [urbit-ob](https://github.com/urbit/urbit-ob), supports only the
4//! `@q` format.
5//!
6//! ## usage
7//!
8//! Note that when encoding more than one byte, `encode` pads from the beginning
9//! to an even number of bytes (as per the original implementation) and `decode`
10//! ignores any dashes, tildes or spaces within the string.
11//! ```rust
12//! urbit_q::encode(&[1]); // nec
13//! let string = urbit_q::encode(&[1, 2, 3]); // doznec-binwes
14//! urbit_q::decode(&string).unwrap(); // [0, 1, 2, 3]
15//! urbit_q::decode("doz nec bin wes"); // Some([0, 1, 2, 3])
16//! urbit_q::decode("do-z ne cb~inwes"); // Some([0, 1, 2, 3])
17//! urbit_q::decode("nec-binwes"); // Some([1, 2, 3])
18//! urbit_q::decode("hello world"); // None
19//! ```
20
21mod consts;
22
23/// Encodes data to Urbit's `@q` format
24///
25/// Note that when encoding more than one byte, it  pads from the beginning to
26/// an even number of bytes (as per the original implementation,
27/// [urbit-ob](https://github.com/urbit/urbit-ob)), e.g.
28/// ```
29/// # use urbit_q::*;
30/// encode(&[1]); // nec
31/// let string = encode(&[1, 2, 3]); // doznec-binwes
32/// decode(&string).unwrap(); // [0, 1, 2, 3]
33/// ```
34pub fn encode(input: &[u8]) -> String {
35    if input.is_empty() {
36        return String::new();
37    }
38    if input.len() == 1 {
39        return String::from(consts::SUFFIXES[input[0] as usize]);
40    }
41    let should_pad = input.len() % 2 != 0;
42    let length = input.len() + should_pad as usize;
43    let dashes = if input.len() > 2 { length / 2 - 1 } else { 0 };
44    let capacity = length * 3 + dashes;
45    let mut output = String::with_capacity(capacity);
46    if should_pad {
47        output.push_str(consts::PREFIXES[0]);
48    }
49    let mut dashes_placed = 0;
50    let iter = input.rchunks_exact(2);
51    let remainder = iter.remainder();
52    if !remainder.is_empty() {
53        output.push_str(consts::SUFFIXES[remainder[0] as usize]);
54        if dashes_placed != dashes {
55            output.push('-');
56            dashes_placed += 1;
57        }
58    }
59    for pair in iter.rev() {
60        output.push_str(consts::PREFIXES[pair[0] as usize]);
61        output.push_str(consts::SUFFIXES[pair[1] as usize]);
62        if dashes_placed != dashes {
63            output.push('-');
64            dashes_placed += 1;
65        }
66    }
67    output
68}
69
70/// Decodes data in Urbit's `@q` format
71///
72/// Note that it ignores any dashes, tildes or spaces within the string, e.g.
73/// ```
74/// # use urbit_q::*;
75/// decode("doznec-binwes"); // Some([0, 1, 2, 3])
76/// decode("doz nec bin wes"); // Some([0, 1, 2, 3])
77/// decode("do-z ne cb~inwes"); // Some([0, 1, 2, 3])
78/// decode("nec-binwes"); // Some([1, 2, 3])
79/// decode("hello world"); // None
80/// ```
81pub fn decode(input: &str) -> Option<Vec<u8>> {
82    let mut bytes = Vec::from(input);
83    bytes.retain(|x| *x != b'-' && *x != b'~' && *x != b' ');
84    match bytes.len() % 6 {
85        0 => {
86            for i in (0..bytes.len()).step_by(6) {
87                let j = i / 3;
88                bytes[j] = *consts::PREFIXES_MAP.get(&bytes[i..i + 3])?;
89                bytes[j + 1] = *consts::SUFFIXES_MAP.get(&bytes[i + 3..i + 6])?;
90            }
91        }
92        3 => {
93            bytes[0] = *consts::SUFFIXES_MAP.get(&bytes[0..3])?;
94            for i in (3..bytes.len()).step_by(6) {
95                let j = i / 3;
96                bytes[j] = *consts::PREFIXES_MAP.get(&bytes[i..i + 3])?;
97                bytes[j + 1] = *consts::SUFFIXES_MAP.get(&bytes[i + 3..i + 6])?;
98            }
99        }
100        _ => return None,
101    }
102    bytes.truncate(bytes.len() / 3);
103    Some(bytes)
104}