1#![cfg_attr(not(feature = "std"), no_std)]
2
3extern crate alloc;
6
7use alloc::string::String;
8use core::fmt;
9
10use lwuri::prelude::*;
11
12#[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#[derive(Clone, Debug, PartialEq, Eq)]
33pub enum UmshUri<'a> {
34 Node(NodeUri<'a>),
35 ChannelByName(ChannelNameUri<'a>),
36 ChannelByKey(ChannelKeyUri<'a>),
37}
38
39#[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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
52pub struct ChannelParams<'a> {
53 pub display_name: Option<&'a str>,
54 pub max_flood_hops: Option<u8>,
55 pub region: Option<&'a str>,
56 pub raw_query: Option<&'a str>,
57}
58
59#[derive(Clone, Copy, Debug, PartialEq, Eq)]
61pub struct ChannelNameUri<'a> {
62 pub name: &'a str,
63 pub params: ChannelParams<'a>,
64}
65
66#[derive(Clone)]
68pub struct ChannelKeyUri<'a> {
69 pub key: umsh_core::ChannelKey,
70 pub params: ChannelParams<'a>,
71}
72
73impl core::fmt::Debug for ChannelKeyUri<'_> {
74 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
75 f.debug_struct("ChannelKeyUri")
76 .field("key", &&self.key.0[..])
77 .field("params", &self.params)
78 .finish()
79 }
80}
81
82impl PartialEq for ChannelKeyUri<'_> {
83 fn eq(&self, other: &Self) -> bool {
84 self.key.0 == other.key.0 && self.params == other.params
85 }
86}
87
88impl Eq for ChannelKeyUri<'_> {}
89
90pub fn parse_umsh_uri<'a>(uri: &'a UriRef) -> Result<UmshUri<'a>, Error> {
92 if uri.scheme() != Some("umsh") {
93 return Err(Error::InvalidUri);
94 }
95
96 let mut parts = uri.raw_path().splitn(3, ':');
97 let kind = parts.next().ok_or(Error::InvalidUri)?;
98 let value = parts.next().ok_or(Error::InvalidUri)?;
99 let tail = parts.next();
100 let params = parse_channel_params(uri)?;
101
102 match kind {
103 "n" => Ok(UmshUri::Node(NodeUri {
104 public_key: umsh_core::PublicKey(decode_base58_32(value)?),
105 identity_data: tail,
106 })),
107 "cs" => Ok(UmshUri::ChannelByName(ChannelNameUri {
108 name: value,
109 params,
110 })),
111 "ck" => Ok(UmshUri::ChannelByKey(ChannelKeyUri {
112 key: umsh_core::ChannelKey(decode_base58_32(value)?),
113 params,
114 })),
115 _ => Err(Error::InvalidUri),
116 }
117}
118
119fn parse_channel_params<'a>(uri: &'a UriRef) -> Result<ChannelParams<'a>, Error> {
120 let mut display_name = None;
121 let mut max_flood_hops = None;
122 let mut region = None;
123
124 for (key, value) in uri.raw_query_key_values() {
125 match key {
126 "n" => display_name = Some(value),
127 "mh" => {
128 max_flood_hops = Some(value.parse::<u8>().map_err(|_| Error::InvalidUri)?);
129 }
130 "r" => region = Some(value),
131 _ => {}
132 }
133 }
134
135 Ok(ChannelParams {
136 display_name,
137 max_flood_hops,
138 region,
139 raw_query: uri.raw_query(),
140 })
141}
142
143fn decode_base58_32(input: &str) -> Result<[u8; 32], Error> {
144 umsh_core::base58::decode(input.as_bytes()).map_err(|err| match err {
145 umsh_core::AddressParseError::InvalidLength => Error::InvalidLength {
146 expected: umsh_core::base58::ENCODED_LEN,
147 actual: input.len(),
148 },
149 _ => Error::InvalidBase58,
150 })
151}
152
153fn encode_base58_32(bytes: &[u8; 32]) -> String {
154 umsh_core::base58::encode(bytes)
155 .iter()
156 .map(|&digit| char::from(digit))
157 .collect()
158}
159
160const BASE58_ALPHABET: &[u8; 58] = b"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
162
163pub fn encode_base58_bytes(bytes: &[u8]) -> String {
168 let zeros = bytes.iter().take_while(|&&byte| byte == 0).count();
169 let mut digits: alloc::vec::Vec<u8> = alloc::vec::Vec::new();
170 for &byte in &bytes[zeros..] {
171 let mut carry = u32::from(byte);
172 for digit in digits.iter_mut() {
173 let acc = (u32::from(*digit) << 8) + carry;
174 *digit = (acc % 58) as u8;
175 carry = acc / 58;
176 }
177 while carry > 0 {
178 digits.push((carry % 58) as u8);
179 carry /= 58;
180 }
181 }
182 let mut out = String::with_capacity(zeros + digits.len());
183 for _ in 0..zeros {
184 out.push('1');
185 }
186 for &digit in digits.iter().rev() {
187 out.push(char::from(BASE58_ALPHABET[usize::from(digit)]));
188 }
189 out
190}
191
192pub fn decode_base58_bytes(input: &str) -> Result<alloc::vec::Vec<u8>, Error> {
195 let bytes = input.as_bytes();
196 let zeros = bytes.iter().take_while(|&&byte| byte == b'1').count();
197 let mut out: alloc::vec::Vec<u8> = alloc::vec::Vec::new();
198 for &digit in &bytes[zeros..] {
199 let value = BASE58_ALPHABET
200 .iter()
201 .position(|&c| c == digit)
202 .ok_or(Error::InvalidBase58)? as u32;
203 let mut carry = value;
204 for byte in out.iter_mut() {
205 let acc = u32::from(*byte) * 58 + carry;
206 *byte = acc as u8;
207 carry = acc >> 8;
208 }
209 while carry > 0 {
210 out.push((carry & 0xFF) as u8);
211 carry >>= 8;
212 }
213 }
214 let mut result = alloc::vec![0u8; zeros];
215 result.extend(out.iter().rev());
216 Ok(result)
217}
218
219pub fn parse_public_key_base58(input: &str) -> Result<umsh_core::PublicKey, Error> {
221 Ok(umsh_core::PublicKey(decode_base58_32(input)?))
222}
223
224pub fn parse_channel_key_base58(input: &str) -> Result<umsh_core::ChannelKey, Error> {
226 Ok(umsh_core::ChannelKey(decode_base58_32(input)?))
227}
228
229pub fn encode_public_key_base58(key: &umsh_core::PublicKey) -> String {
231 encode_base58_32(&key.0)
232}
233
234pub fn encode_channel_key_base58(key: &umsh_core::ChannelKey) -> String {
236 encode_base58_32(&key.0)
237}
238
239pub fn format_node_uri(key: &umsh_core::PublicKey, buf: &mut [u8]) -> Result<usize, Error> {
240 let mut pos = 0usize;
241 copy_into(buf, &mut pos, b"umsh:n:")?;
242 copy_into(buf, &mut pos, &umsh_core::base58::encode(&key.0))?;
243 Ok(pos)
244}
245
246pub fn format_channel_name_uri(name: &str, buf: &mut [u8]) -> Result<usize, Error> {
252 let mut pos = 0usize;
253 copy_into(buf, &mut pos, b"umsh:cs:")?;
254 write_percent_encoded(buf, &mut pos, name)?;
255 Ok(pos)
256}
257
258pub fn format_channel_key_uri(key: &umsh_core::ChannelKey, buf: &mut [u8]) -> Result<usize, Error> {
259 let mut pos = 0usize;
260 copy_into(buf, &mut pos, b"umsh:ck:")?;
261 copy_into(buf, &mut pos, &umsh_core::base58::encode(&key.0))?;
262 Ok(pos)
263}
264
265pub fn format_channel_name_uri_with_params(
266 name: &str,
267 params: &ChannelParams<'_>,
268 buf: &mut [u8],
269) -> Result<usize, Error> {
270 let mut pos = format_channel_name_uri(name, buf)?;
271 write_params(params, buf, &mut pos)?;
272 Ok(pos)
273}
274
275pub fn format_channel_key_uri_with_params(
276 key: &umsh_core::ChannelKey,
277 params: &ChannelParams<'_>,
278 buf: &mut [u8],
279) -> Result<usize, Error> {
280 let mut pos = format_channel_key_uri(key, buf)?;
281 write_params(params, buf, &mut pos)?;
282 Ok(pos)
283}
284
285fn write_params(params: &ChannelParams<'_>, buf: &mut [u8], pos: &mut usize) -> Result<(), Error> {
286 let mut wrote = false;
287 if let Some(display_name) = params.display_name {
288 push_byte(buf, pos, if wrote { b';' } else { b'?' })?;
289 wrote = true;
290 copy_into(buf, pos, b"n=")?;
291 write_percent_encoded(buf, pos, display_name)?;
292 }
293 if let Some(max_flood_hops) = params.max_flood_hops {
294 push_byte(buf, pos, if wrote { b';' } else { b'?' })?;
295 wrote = true;
296 copy_into(buf, pos, b"mh=")?;
297 let mut tmp = [0u8; 3];
298 let digits = write_decimal_u8(max_flood_hops, &mut tmp);
299 copy_into(buf, pos, &tmp[..digits])?;
300 }
301 if let Some(region) = params.region {
302 push_byte(buf, pos, if wrote { b';' } else { b'?' })?;
303 copy_into(buf, pos, b"r=")?;
304 write_percent_encoded(buf, pos, region)?;
305 }
306 Ok(())
307}
308
309fn is_unreserved(byte: u8) -> bool {
315 byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'.' | b'_' | b'~')
316}
317
318fn write_percent_encoded(buf: &mut [u8], pos: &mut usize, value: &str) -> Result<(), Error> {
319 const HEX: &[u8; 16] = b"0123456789ABCDEF";
320 for &byte in value.as_bytes() {
321 if is_unreserved(byte) {
322 push_byte(buf, pos, byte)?;
323 } else {
324 copy_into(
325 buf,
326 pos,
327 &[b'%', HEX[(byte >> 4) as usize], HEX[(byte & 0x0f) as usize]],
328 )?;
329 }
330 }
331 Ok(())
332}
333
334pub fn decode_percent(value: &str) -> Result<String, Error> {
340 let bytes = value.as_bytes();
341 let mut out = alloc::vec::Vec::with_capacity(bytes.len());
342 let mut index = 0usize;
343 while index < bytes.len() {
344 match (bytes[index], bytes.get(index + 1), bytes.get(index + 2)) {
345 (b'%', Some(high), Some(low)) => match (hex_value(*high), hex_value(*low)) {
346 (Some(high), Some(low)) => {
347 out.push((high << 4) | low);
348 index += 3;
349 }
350 _ => {
351 out.push(b'%');
352 index += 1;
353 }
354 },
355 (byte, _, _) => {
356 out.push(byte);
357 index += 1;
358 }
359 }
360 }
361 String::from_utf8(out).map_err(|_| Error::InvalidUtf8)
362}
363
364fn hex_value(byte: u8) -> Option<u8> {
365 match byte {
366 b'0'..=b'9' => Some(byte - b'0'),
367 b'a'..=b'f' => Some(byte - b'a' + 10),
368 b'A'..=b'F' => Some(byte - b'A' + 10),
369 _ => None,
370 }
371}
372
373fn write_decimal_u8(value: u8, out: &mut [u8; 3]) -> usize {
374 if value >= 100 {
375 out[0] = b'0' + value / 100;
376 out[1] = b'0' + (value / 10) % 10;
377 out[2] = b'0' + value % 10;
378 3
379 } else if value >= 10 {
380 out[0] = b'0' + value / 10;
381 out[1] = b'0' + value % 10;
382 2
383 } else {
384 out[0] = b'0' + value;
385 1
386 }
387}
388
389fn copy_into(dst: &mut [u8], pos: &mut usize, src: &[u8]) -> Result<(), Error> {
390 if dst.len().saturating_sub(*pos) < src.len() {
391 return Err(Error::BufferTooSmall);
392 }
393 dst[*pos..*pos + src.len()].copy_from_slice(src);
394 *pos += src.len();
395 Ok(())
396}
397
398fn push_byte(dst: &mut [u8], pos: &mut usize, byte: u8) -> Result<(), Error> {
399 copy_into(dst, pos, &[byte])
400}
401
402#[cfg(test)]
403mod tests {
404 use lwuri::UriRef;
405
406 use super::*;
407
408 #[test]
409 fn variable_base58_round_trips_and_matches_fixed_width() {
410 for bytes in [
411 &[][..],
412 &[0u8][..],
413 &[0, 0, 1, 2, 3][..],
414 &[0xFF; 100][..],
415 b"role-caps-options-and-signature".as_slice(),
416 ] {
417 let encoded = encode_base58_bytes(bytes);
418 assert_eq!(decode_base58_bytes(&encoded).unwrap(), bytes);
419 }
420
421 let key = [0xFFu8; 32];
424 let fixed: alloc::string::String = umsh_core::base58::encode(&key)
425 .into_iter()
426 .map(char::from)
427 .collect();
428 assert_eq!(encode_base58_bytes(&key), fixed);
429
430 assert_eq!(
431 decode_base58_bytes("not-base58!"),
432 Err(Error::InvalidBase58)
433 );
434 }
435
436 #[test]
437 fn uri_parse_and_format_cover_node_channel_name_and_key() {
438 let key = umsh_core::PublicKey([0x33; 32]);
439 let mut buf = [0u8; 128];
440 let node_len = format_node_uri(&key, &mut buf).unwrap();
441 let node_uri = UriRef::from_str(core::str::from_utf8(&buf[..node_len]).unwrap()).unwrap();
442 match parse_umsh_uri(node_uri).unwrap() {
443 UmshUri::Node(parsed) => assert_eq!(parsed.public_key, key),
444 _ => panic!("expected node uri"),
445 }
446
447 let params = ChannelParams {
448 display_name: Some("Local"),
449 max_flood_hops: Some(6),
450 region: Some("Eugine"),
451 raw_query: None,
452 };
453 let channel_name_len =
454 format_channel_name_uri_with_params("Public", ¶ms, &mut buf).unwrap();
455 let channel_name_uri =
456 UriRef::from_str(core::str::from_utf8(&buf[..channel_name_len]).unwrap()).unwrap();
457 match parse_umsh_uri(channel_name_uri).unwrap() {
458 UmshUri::ChannelByName(parsed) => {
459 assert_eq!(parsed.name, "Public");
460 assert_eq!(parsed.params.display_name, Some("Local"));
461 assert_eq!(parsed.params.max_flood_hops, Some(6));
462 assert_eq!(parsed.params.region, Some("Eugine"));
463 }
464 _ => panic!("expected channel name uri"),
465 }
466
467 let channel_key = umsh_core::ChannelKey([0x44; 32]);
468 let channel_key_len = format_channel_key_uri(&channel_key, &mut buf).unwrap();
469 let channel_key_uri =
470 UriRef::from_str(core::str::from_utf8(&buf[..channel_key_len]).unwrap()).unwrap();
471 match parse_umsh_uri(channel_key_uri).unwrap() {
472 UmshUri::ChannelByKey(parsed) => assert_eq!(parsed.key.0, channel_key.0),
473 _ => panic!("expected channel key uri"),
474 }
475 }
476
477 #[test]
478 fn channel_key_uri_carries_invitation_params() {
479 let channel_key = umsh_core::ChannelKey([0x55; 32]);
480 let params = ChannelParams {
481 display_name: Some("Trail Crew"),
482 max_flood_hops: Some(3),
483 region: Some("SJC"),
484 raw_query: None,
485 };
486 let mut buf = [0u8; 128];
487 let len = format_channel_key_uri_with_params(&channel_key, ¶ms, &mut buf).unwrap();
488 let uri = UriRef::from_str(core::str::from_utf8(&buf[..len]).unwrap()).unwrap();
489 match parse_umsh_uri(uri).unwrap() {
490 UmshUri::ChannelByKey(parsed) => {
491 assert_eq!(parsed.key.0, channel_key.0);
492 assert_eq!(parsed.params.display_name, Some("Trail%20Crew"));
495 assert_eq!(
496 decode_percent(parsed.params.display_name.unwrap()).unwrap(),
497 "Trail Crew"
498 );
499 assert_eq!(parsed.params.max_flood_hops, Some(3));
500 assert_eq!(parsed.params.region, Some("SJC"));
501 }
502 _ => panic!("expected channel key uri"),
503 }
504 }
505
506 #[test]
507 fn param_values_escape_separators_and_round_trip() {
508 let channel_key = umsh_core::ChannelKey([0x88; 32]);
511 let hostile = "a?b;c=d&e/f";
512 let params = ChannelParams {
513 display_name: Some(hostile),
514 max_flood_hops: None,
515 region: None,
516 raw_query: None,
517 };
518 let mut buf = [0u8; 160];
519 let len = format_channel_key_uri_with_params(&channel_key, ¶ms, &mut buf).unwrap();
520 let uri = UriRef::from_str(core::str::from_utf8(&buf[..len]).unwrap()).unwrap();
521 match parse_umsh_uri(uri).unwrap() {
522 UmshUri::ChannelByKey(parsed) => {
523 assert_eq!(parsed.params.max_flood_hops, None);
524 assert_eq!(parsed.params.region, None);
525 assert_eq!(
526 decode_percent(parsed.params.display_name.unwrap()).unwrap(),
527 hostile
528 );
529 }
530 _ => panic!("expected channel key uri"),
531 }
532 }
533
534 #[test]
535 fn decode_percent_preserves_malformed_escapes() {
536 assert_eq!(decode_percent("plain").unwrap(), "plain");
537 assert_eq!(decode_percent("a%2Fb").unwrap(), "a/b");
538 assert_eq!(decode_percent("100%").unwrap(), "100%");
539 assert_eq!(decode_percent("%zz").unwrap(), "%zz");
540 assert_eq!(decode_percent("%e2%82%ac").unwrap(), "€");
541 assert_eq!(decode_percent("%ff"), Err(Error::InvalidUtf8));
542 }
543
544 #[test]
545 fn channel_key_uri_without_params_matches_the_bare_form() {
546 let channel_key = umsh_core::ChannelKey([0x66; 32]);
547 let empty = ChannelParams {
548 display_name: None,
549 max_flood_hops: None,
550 region: None,
551 raw_query: None,
552 };
553 let mut with_params = [0u8; 128];
554 let mut bare = [0u8; 128];
555 let with_len =
556 format_channel_key_uri_with_params(&channel_key, &empty, &mut with_params).unwrap();
557 let bare_len = format_channel_key_uri(&channel_key, &mut bare).unwrap();
558 assert_eq!(&with_params[..with_len], &bare[..bare_len]);
559 }
560
561 #[test]
562 fn channel_key_uri_with_params_reports_a_short_buffer() {
563 let channel_key = umsh_core::ChannelKey([0x77; 32]);
564 let params = ChannelParams {
565 display_name: Some("Trail Crew"),
566 max_flood_hops: None,
567 region: None,
568 raw_query: None,
569 };
570 let mut buf = [0u8; 52];
572 assert_eq!(
573 format_channel_key_uri_with_params(&channel_key, ¶ms, &mut buf),
574 Err(Error::BufferTooSmall)
575 );
576 }
577
578 #[test]
579 fn base58_key_helpers_round_trip() {
580 let public_key = umsh_core::PublicKey([0x33; 32]);
581 let channel_key = umsh_core::ChannelKey([0x77; 32]);
582
583 let public_text = encode_public_key_base58(&public_key);
584 let channel_text = encode_channel_key_base58(&channel_key);
585
586 assert_eq!(parse_public_key_base58(&public_text).unwrap(), public_key);
587 assert_eq!(
588 parse_channel_key_base58(&channel_text).unwrap(),
589 channel_key
590 );
591 }
592}