umsh_uri/
lib.rs

1#![cfg_attr(not(feature = "std"), no_std)]
2
3//! UMSH URI parsing and formatting helpers.
4
5extern crate alloc;
6
7use alloc::string::String;
8use core::fmt;
9
10use lwuri::prelude::*;
11
12/// Error returned when parsing or formatting `umsh:` URIs.
13#[derive(Clone, Copy, Debug, PartialEq, Eq)]
14pub enum Error {
15    InvalidUtf8,
16    InvalidUri,
17    InvalidBase58,
18    InvalidLength { expected: usize, actual: usize },
19    BufferTooSmall,
20}
21
22impl fmt::Display for Error {
23    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
24        write!(f, "{self:?}")
25    }
26}
27
28#[cfg(feature = "std")]
29impl std::error::Error for Error {}
30
31/// Parsed `umsh:` URI.
32#[derive(Clone, Debug, PartialEq, Eq)]
33pub enum UmshUri<'a> {
34    Node(NodeUri<'a>),
35    ChannelByName(ChannelNameUri<'a>),
36    ChannelByKey(ChannelKeyUri<'a>),
37}
38
39/// Parsed node URI.
40#[derive(Clone, Copy, Debug, PartialEq, Eq)]
41pub struct NodeUri<'a> {
42    pub public_key: umsh_core::PublicKey,
43    pub identity_data: Option<&'a str>,
44}
45
46/// Advisory channel metadata decoded from a URI query string.
47#[derive(Clone, Copy, Debug, PartialEq, Eq)]
48pub struct ChannelParams<'a> {
49    pub display_name: Option<&'a str>,
50    pub max_flood_hops: Option<u8>,
51    pub region: Option<&'a str>,
52    pub raw_query: Option<&'a str>,
53}
54
55/// Parsed named-channel URI.
56#[derive(Clone, Copy, Debug, PartialEq, Eq)]
57pub struct ChannelNameUri<'a> {
58    pub name: &'a str,
59    pub params: ChannelParams<'a>,
60}
61
62/// Parsed direct-key channel URI.
63#[derive(Clone)]
64pub struct ChannelKeyUri<'a> {
65    pub key: umsh_core::ChannelKey,
66    pub params: ChannelParams<'a>,
67}
68
69impl core::fmt::Debug for ChannelKeyUri<'_> {
70    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
71        f.debug_struct("ChannelKeyUri")
72            .field("key", &&self.key.0[..])
73            .field("params", &self.params)
74            .finish()
75    }
76}
77
78impl PartialEq for ChannelKeyUri<'_> {
79    fn eq(&self, other: &Self) -> bool {
80        self.key.0 == other.key.0 && self.params == other.params
81    }
82}
83
84impl Eq for ChannelKeyUri<'_> {}
85
86/// Parse a `umsh:` URI reference.
87pub fn parse_umsh_uri<'a>(uri: &'a UriRef) -> Result<UmshUri<'a>, Error> {
88    if uri.scheme() != Some("umsh") {
89        return Err(Error::InvalidUri);
90    }
91
92    let mut parts = uri.raw_path().splitn(3, ':');
93    let kind = parts.next().ok_or(Error::InvalidUri)?;
94    let value = parts.next().ok_or(Error::InvalidUri)?;
95    let tail = parts.next();
96    let params = parse_channel_params(uri)?;
97
98    match kind {
99        "n" => Ok(UmshUri::Node(NodeUri {
100            public_key: umsh_core::PublicKey(decode_base58_32(value)?),
101            identity_data: tail,
102        })),
103        "cs" => Ok(UmshUri::ChannelByName(ChannelNameUri {
104            name: value,
105            params,
106        })),
107        "ck" => Ok(UmshUri::ChannelByKey(ChannelKeyUri {
108            key: umsh_core::ChannelKey(decode_base58_32(value)?),
109            params,
110        })),
111        _ => Err(Error::InvalidUri),
112    }
113}
114
115fn parse_channel_params<'a>(uri: &'a UriRef) -> Result<ChannelParams<'a>, Error> {
116    let mut display_name = None;
117    let mut max_flood_hops = None;
118    let mut region = None;
119
120    for (key, value) in uri.raw_query_key_values() {
121        match key {
122            "n" => display_name = Some(value),
123            "mh" => {
124                max_flood_hops = Some(value.parse::<u8>().map_err(|_| Error::InvalidUri)?);
125            }
126            "r" => region = Some(value),
127            _ => {}
128        }
129    }
130
131    Ok(ChannelParams {
132        display_name,
133        max_flood_hops,
134        region,
135        raw_query: uri.raw_query(),
136    })
137}
138
139fn decode_base58_32(input: &str) -> Result<[u8; 32], Error> {
140    let mut out = [0u8; 32];
141    let len = bs58::decode(input)
142        .onto(&mut out)
143        .map_err(|_| Error::InvalidBase58)?;
144    if len != 32 {
145        return Err(Error::InvalidLength {
146            expected: 32,
147            actual: len,
148        });
149    }
150    Ok(out)
151}
152
153/// Parse a base58-encoded public key.
154pub fn parse_public_key_base58(input: &str) -> Result<umsh_core::PublicKey, Error> {
155    Ok(umsh_core::PublicKey(decode_base58_32(input)?))
156}
157
158/// Parse a base58-encoded channel key.
159pub fn parse_channel_key_base58(input: &str) -> Result<umsh_core::ChannelKey, Error> {
160    Ok(umsh_core::ChannelKey(decode_base58_32(input)?))
161}
162
163/// Encode a public key as base58.
164pub fn encode_public_key_base58(key: &umsh_core::PublicKey) -> String {
165    bs58::encode(key.0).into_string()
166}
167
168/// Encode a channel key as base58.
169pub fn encode_channel_key_base58(key: &umsh_core::ChannelKey) -> String {
170    bs58::encode(key.0).into_string()
171}
172
173pub fn format_node_uri(key: &umsh_core::PublicKey, buf: &mut [u8]) -> Result<usize, Error> {
174    let mut pos = 0usize;
175    copy_into(buf, &mut pos, b"umsh:n:")?;
176    let written = bs58::encode(key.0)
177        .onto(&mut buf[pos..])
178        .map_err(|_| Error::BufferTooSmall)?;
179    Ok(pos + written)
180}
181
182pub fn format_channel_name_uri(name: &str, buf: &mut [u8]) -> Result<usize, Error> {
183    let mut pos = 0usize;
184    copy_into(buf, &mut pos, b"umsh:cs:")?;
185    copy_into(buf, &mut pos, name.as_bytes())?;
186    Ok(pos)
187}
188
189pub fn format_channel_key_uri(key: &umsh_core::ChannelKey, buf: &mut [u8]) -> Result<usize, Error> {
190    let mut pos = 0usize;
191    copy_into(buf, &mut pos, b"umsh:ck:")?;
192    let written = bs58::encode(&key.0)
193        .onto(&mut buf[pos..])
194        .map_err(|_| Error::BufferTooSmall)?;
195    Ok(pos + written)
196}
197
198pub fn format_channel_name_uri_with_params(
199    name: &str,
200    params: &ChannelParams<'_>,
201    buf: &mut [u8],
202) -> Result<usize, Error> {
203    let mut pos = format_channel_name_uri(name, buf)?;
204    write_params(params, buf, &mut pos)?;
205    Ok(pos)
206}
207
208fn write_params(params: &ChannelParams<'_>, buf: &mut [u8], pos: &mut usize) -> Result<(), Error> {
209    let mut wrote = false;
210    if let Some(display_name) = params.display_name {
211        push_byte(buf, pos, if wrote { b';' } else { b'?' })?;
212        wrote = true;
213        copy_into(buf, pos, b"n=")?;
214        copy_into(buf, pos, display_name.as_bytes())?;
215    }
216    if let Some(max_flood_hops) = params.max_flood_hops {
217        push_byte(buf, pos, if wrote { b';' } else { b'?' })?;
218        wrote = true;
219        copy_into(buf, pos, b"mh=")?;
220        let mut tmp = [0u8; 3];
221        let digits = write_decimal_u8(max_flood_hops, &mut tmp);
222        copy_into(buf, pos, &tmp[..digits])?;
223    }
224    if let Some(region) = params.region {
225        push_byte(buf, pos, if wrote { b';' } else { b'?' })?;
226        copy_into(buf, pos, b"r=")?;
227        copy_into(buf, pos, region.as_bytes())?;
228    }
229    Ok(())
230}
231
232fn write_decimal_u8(value: u8, out: &mut [u8; 3]) -> usize {
233    if value >= 100 {
234        out[0] = b'0' + value / 100;
235        out[1] = b'0' + (value / 10) % 10;
236        out[2] = b'0' + value % 10;
237        3
238    } else if value >= 10 {
239        out[0] = b'0' + value / 10;
240        out[1] = b'0' + value % 10;
241        2
242    } else {
243        out[0] = b'0' + value;
244        1
245    }
246}
247
248fn copy_into(dst: &mut [u8], pos: &mut usize, src: &[u8]) -> Result<(), Error> {
249    if dst.len().saturating_sub(*pos) < src.len() {
250        return Err(Error::BufferTooSmall);
251    }
252    dst[*pos..*pos + src.len()].copy_from_slice(src);
253    *pos += src.len();
254    Ok(())
255}
256
257fn push_byte(dst: &mut [u8], pos: &mut usize, byte: u8) -> Result<(), Error> {
258    copy_into(dst, pos, &[byte])
259}
260
261#[cfg(test)]
262mod tests {
263    use lwuri::UriRef;
264
265    use super::*;
266
267    #[test]
268    fn uri_parse_and_format_cover_node_channel_name_and_key() {
269        let key = umsh_core::PublicKey([0x33; 32]);
270        let mut buf = [0u8; 128];
271        let node_len = format_node_uri(&key, &mut buf).unwrap();
272        let node_uri = UriRef::from_str(core::str::from_utf8(&buf[..node_len]).unwrap()).unwrap();
273        match parse_umsh_uri(node_uri).unwrap() {
274            UmshUri::Node(parsed) => assert_eq!(parsed.public_key, key),
275            _ => panic!("expected node uri"),
276        }
277
278        let params = ChannelParams {
279            display_name: Some("Local"),
280            max_flood_hops: Some(6),
281            region: Some("Eugine"),
282            raw_query: None,
283        };
284        let channel_name_len =
285            format_channel_name_uri_with_params("Public", &params, &mut buf).unwrap();
286        let channel_name_uri =
287            UriRef::from_str(core::str::from_utf8(&buf[..channel_name_len]).unwrap()).unwrap();
288        match parse_umsh_uri(channel_name_uri).unwrap() {
289            UmshUri::ChannelByName(parsed) => {
290                assert_eq!(parsed.name, "Public");
291                assert_eq!(parsed.params.display_name, Some("Local"));
292                assert_eq!(parsed.params.max_flood_hops, Some(6));
293                assert_eq!(parsed.params.region, Some("Eugine"));
294            }
295            _ => panic!("expected channel name uri"),
296        }
297
298        let channel_key = umsh_core::ChannelKey([0x44; 32]);
299        let channel_key_len = format_channel_key_uri(&channel_key, &mut buf).unwrap();
300        let channel_key_uri =
301            UriRef::from_str(core::str::from_utf8(&buf[..channel_key_len]).unwrap()).unwrap();
302        match parse_umsh_uri(channel_key_uri).unwrap() {
303            UmshUri::ChannelByKey(parsed) => assert_eq!(parsed.key.0, channel_key.0),
304            _ => panic!("expected channel key uri"),
305        }
306    }
307
308    #[test]
309    fn base58_key_helpers_round_trip() {
310        let public_key = umsh_core::PublicKey([0x33; 32]);
311        let channel_key = umsh_core::ChannelKey([0x77; 32]);
312
313        let public_text = encode_public_key_base58(&public_key);
314        let channel_text = encode_channel_key_base58(&channel_key);
315
316        assert_eq!(parse_public_key_base58(&public_text).unwrap(), public_key);
317        assert_eq!(
318            parse_channel_key_base58(&channel_text).unwrap(),
319            channel_key
320        );
321    }
322}