use alloc::{string::String, vec::Vec};
use crate::bytewords::{self, Style};
use crate::fountain::{self, DecoderLimits};
use crate::{Error, Result};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct UrType(String);
impl UrType {
pub fn new(s: &str) -> Result<Self> {
let lower = s.to_ascii_lowercase();
validate_type(&lower)?;
Ok(Self(lower))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn bytes() -> Self {
Self(String::from("bytes"))
}
}
impl TryFrom<&str> for UrType {
type Error = Error;
fn try_from(value: &str) -> Result<Self> {
Self::new(value)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Kind {
SinglePart,
MultiPart,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParsedUr {
pub ur_type: UrType,
pub kind: Kind,
pub indices: Option<(u32, u32)>,
pub body: String,
}
#[must_use]
pub fn normalize_ur(uri: &str) -> String {
uri.to_ascii_lowercase()
}
pub fn encode(data: &[u8], ur_type: &UrType) -> Result<String> {
let body = bytewords::encode(data, Style::Minimal);
Ok(alloc::format!("ur:{}/{body}", ur_type.as_str()))
}
pub fn decode(uri: &str) -> Result<(Kind, Vec<u8>)> {
let (kind, payload, _) = decode_with_indices(uri)?;
Ok((kind, payload))
}
pub fn decode_with_type(uri: &str) -> Result<(UrType, Kind, Vec<u8>)> {
let parsed = parse(uri)?;
let payload = bytewords::decode(&parsed.body, Style::Minimal)?;
Ok((parsed.ur_type, parsed.kind, payload))
}
pub fn parse(uri: &str) -> Result<ParsedUr> {
let lowered = normalize_ur(uri);
parse_normalized(&lowered)
}
pub fn parse_normalized(uri: &str) -> Result<ParsedUr> {
let strip_scheme = uri.strip_prefix("ur:").ok_or(Error::InvalidScheme)?;
let (type_str, rest) = strip_scheme.split_once('/').ok_or(Error::TypeUnspecified)?;
let ur_type = UrType::new(type_str)?;
match rest.rsplit_once('/') {
None => Ok(ParsedUr {
ur_type,
kind: Kind::SinglePart,
indices: None,
body: rest.to_ascii_lowercase(),
}),
Some((indices, body)) => {
let indices = decode_indices(indices)?;
Ok(ParsedUr {
ur_type,
kind: Kind::MultiPart,
indices: Some(indices),
body: body.to_ascii_lowercase(),
})
}
}
}
type DecodedPayload = (Kind, Vec<u8>, Option<(u32, u32)>);
fn decode_with_indices(value: &str) -> Result<DecodedPayload> {
let parsed = parse(value)?;
let payload = bytewords::decode(&parsed.body, Style::Minimal)?;
Ok((parsed.kind, payload, parsed.indices))
}
fn decode_indices(indices: &str) -> Result<(u32, u32)> {
let (idx, idx_total) = indices.split_once('-').ok_or(Error::InvalidIndices)?;
let idx = idx.parse::<u32>().map_err(|_| Error::InvalidIndices)?;
let idx_total = idx_total
.parse::<u32>()
.map_err(|_| Error::InvalidIndices)?;
if idx == 0 || idx_total == 0 {
return Err(Error::InvalidIndices);
}
Ok((idx, idx_total))
}
fn validate_type(s: &str) -> Result<()> {
if s.is_empty() || !s.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'-') {
return Err(Error::InvalidType);
}
Ok(())
}
#[derive(Debug)]
pub struct Encoder {
fountain: fountain::Encoder,
ur_type: UrType,
}
impl Encoder {
pub fn bytes(message: &[u8], max_fragment_length: usize) -> Result<Self> {
Self::new(message, max_fragment_length, &UrType::bytes())
}
pub fn new(message: &[u8], max_fragment_length: usize, ur_type: &UrType) -> Result<Self> {
Ok(Self {
fountain: fountain::Encoder::new(message, max_fragment_length)?,
ur_type: ur_type.clone(),
})
}
pub fn next_part(&mut self) -> Result<String> {
let part = self.fountain.next_part()?;
let body = bytewords::encode(&part.to_cbor(), Style::Minimal);
Ok(alloc::format!(
"ur:{}/{}/{body}",
self.ur_type.as_str(),
part.sequence_id()
))
}
#[must_use]
pub const fn current_index(&self) -> u32 {
self.fountain.current_sequence()
}
#[must_use]
pub const fn fragment_count(&self) -> u32 {
self.fountain.fragment_count()
}
}
#[derive(Debug)]
pub struct Decoder {
fountain: fountain::Decoder,
max_uri_len: usize,
expected_type: Option<UrType>,
seen_type: Option<UrType>,
poisoned: Option<&'static str>,
}
impl Default for Decoder {
fn default() -> Self {
Self::new()
}
}
impl Decoder {
#[must_use]
pub fn new() -> Self {
Self::with_limits(DecoderLimits::default())
}
#[must_use]
pub const fn with_limits(limits: DecoderLimits) -> Self {
Self {
fountain: fountain::Decoder::with_limits(limits),
max_uri_len: limits.max_uri_len,
expected_type: None,
seen_type: None,
poisoned: None,
}
}
#[must_use]
pub fn with_expected_type(mut self, ur_type: UrType) -> Self {
self.expected_type = Some(ur_type);
self
}
const fn poison(&mut self, reason: &'static str) -> Error {
self.poisoned = Some(reason);
Error::ResourceLimit(reason)
}
fn map_resource_limit(&mut self, err: Error) -> Error {
match err {
Error::ResourceLimit(reason) => self.poison(reason),
other => other,
}
}
pub fn receive(&mut self, value: &str) -> Result<()> {
if let Some(reason) = self.poisoned {
return Err(Error::ResourceLimit(reason));
}
if value.len() > self.max_uri_len {
return Err(self.poison("uri_len"));
}
let parsed = parse(value)?;
if parsed.kind != Kind::MultiPart {
return Err(Error::NotMultiPart);
}
if let Some(ref expected) = self.expected_type {
if &parsed.ur_type != expected {
return Err(Error::UnexpectedType {
expected: String::from(expected.as_str()),
found: String::from(parsed.ur_type.as_str()),
});
}
}
match &self.seen_type {
None => self.seen_type = Some(parsed.ur_type.clone()),
Some(seen) if seen != &parsed.ur_type => {
return Err(Error::UnexpectedType {
expected: String::from(seen.as_str()),
found: String::from(parsed.ur_type.as_str()),
});
}
Some(_) => {}
}
let decoded = bytewords::decode(&parsed.body, Style::Minimal)?;
let part = fountain::Part::from_cbor_with_max(
decoded.as_slice(),
self.fountain.max_fragment_data_length(),
)
.map_err(|e| self.map_resource_limit(e))?;
let (idx, idx_total) = parsed.indices.ok_or(Error::InvalidIndices)?;
if part.sequence() != idx || part.sequence_count() != idx_total {
return Err(Error::InvalidIndices);
}
self.fountain
.receive(part)
.map_err(|e| self.map_resource_limit(e))?;
Ok(())
}
#[must_use]
pub fn complete(&self) -> bool {
self.fountain.complete()
}
pub fn message(&self) -> Result<Option<Vec<u8>>> {
if let Some(reason) = self.poisoned {
return Err(Error::ResourceLimit(reason));
}
self.fountain.message()
}
#[must_use]
pub fn resolved_fragment_count(&self) -> Option<usize> {
self.fountain.resolved_fragment_count()
}
#[must_use]
pub const fn fragment_count(&self) -> usize {
self.fountain.fragment_count()
}
#[must_use]
pub const fn is_poisoned(&self) -> bool {
self.poisoned.is_some() || self.fountain.is_poisoned()
}
}
#[must_use]
pub fn qr_string(ur: &str) -> String {
ur.to_ascii_uppercase()
}
#[cfg(test)]
mod tests {
use minicbor::bytes::ByteVec;
use super::*;
use crate::fountain::DecoderLimits;
use crate::rng::test_utils::make_message;
fn make_message_ur(length: usize, seed: &str) -> Vec<u8> {
let message = make_message(seed, length);
minicbor::to_vec(ByteVec::from(message)).unwrap()
}
#[test]
fn test_single_part_ur() {
let ur = make_message_ur(50, "Wolf");
let encoded = encode(&ur, &UrType::bytes()).unwrap();
let expected = "ur:bytes/hdeymejtswhhylkepmykhhtsytsnoyoyaxaedsuttydmmhhpktpmsrjtgwdpfnsboxgwlbaawzuefywkdplrsrjynbvygabwjldapfcsdwkbrkch";
assert_eq!(encoded, expected);
let decoded = decode(&encoded).unwrap();
assert_eq!((Kind::SinglePart, ur), decoded);
}
#[test]
fn test_ur_encoder() {
let ur = make_message_ur(256, "Wolf");
let mut encoder = Encoder::bytes(&ur, 30).unwrap();
let expected = [
"ur:bytes/1-9/lpadascfadaxcywenbpljkhdcahkadaemejtswhhylkepmykhhtsytsnoyoyaxaedsuttydmmhhpktpmsrjtdkgslpgh",
"ur:bytes/2-9/lpaoascfadaxcywenbpljkhdcagwdpfnsboxgwlbaawzuefywkdplrsrjynbvygabwjldapfcsgmghhkhstlrdcxaefz",
"ur:bytes/3-9/lpaxascfadaxcywenbpljkhdcahelbknlkuejnbadmssfhfrdpsbiegecpasvssovlgeykssjykklronvsjksopdzmol",
"ur:bytes/4-9/lpaaascfadaxcywenbpljkhdcasotkhemthydawydtaxneurlkosgwcekonertkbrlwmplssjtammdplolsbrdzcrtas",
"ur:bytes/5-9/lpahascfadaxcywenbpljkhdcatbbdfmssrkzmcwnezelennjpfzbgmuktrhtejscktelgfpdlrkfyfwdajldejokbwf",
"ur:bytes/6-9/lpamascfadaxcywenbpljkhdcackjlhkhybssklbwefectpfnbbectrljectpavyrolkzczcpkmwidmwoxkilghdsowp",
"ur:bytes/7-9/lpatascfadaxcywenbpljkhdcavszmwnjkwtclrtvaynhpahrtoxmwvwatmedibkaegdosftvandiodagdhthtrlnnhy",
"ur:bytes/8-9/lpayascfadaxcywenbpljkhdcadmsponkkbbhgsoltjntegepmttmoonftnbuoiyrehfrtsabzsttorodklubbuyaetk",
"ur:bytes/9-9/lpasascfadaxcywenbpljkhdcajskecpmdckihdyhphfotjojtfmlnwmadspaxrkytbztpbauotbgtgtaeaevtgavtny",
"ur:bytes/10-9/lpbkascfadaxcywenbpljkhdcahkadaemejtswhhylkepmykhhtsytsnoyoyaxaedsuttydmmhhpktpmsrjtwdkiplzs",
"ur:bytes/11-9/lpbdascfadaxcywenbpljkhdcahelbknlkuejnbadmssfhfrdpsbiegecpasvssovlgeykssjykklronvsjkvetiiapk",
"ur:bytes/12-9/lpbnascfadaxcywenbpljkhdcarllaluzmdmgstospeyiefmwejlwtpedamktksrvlcygmzemovovllarodtmtbnptrs",
"ur:bytes/13-9/lpbtascfadaxcywenbpljkhdcamtkgtpknghchchyketwsvwgwfdhpgmgtylctotzopdrpayoschcmhplffziachrfgd",
"ur:bytes/14-9/lpbaascfadaxcywenbpljkhdcapazewnvonnvdnsbyleynwtnsjkjndeoldydkbkdslgjkbbkortbelomueekgvstegt",
"ur:bytes/15-9/lpbsascfadaxcywenbpljkhdcaynmhpddpzmversbdqdfyrehnqzlugmjzmnmtwmrouohtstgsbsahpawkditkckynwt",
"ur:bytes/16-9/lpbeascfadaxcywenbpljkhdcawygekobamwtlihsnpalnsghenskkiynthdzotsimtojetprsttmukirlrsbtamjtpd",
"ur:bytes/17-9/lpbyascfadaxcywenbpljkhdcamklgftaxykpewyrtqzhydntpnytyisincxmhtbceaykolduortotiaiaiafhiaoyce",
"ur:bytes/18-9/lpbgascfadaxcywenbpljkhdcahkadaemejtswhhylkepmykhhtsytsnoyoyaxaedsuttydmmhhpktpmsrjtntwkbkwy",
"ur:bytes/19-9/lpbwascfadaxcywenbpljkhdcadekicpaajootjzpsdrbalpeywllbdsnbinaerkurspbncxgslgftvtsrjtksplcpeo",
"ur:bytes/20-9/lpbbascfadaxcywenbpljkhdcayapmrleeleaxpasfrtrdkncffwjyjzgyetdmlewtkpktgllepfrltataztksmhkbot",
];
assert_eq!(encoder.fragment_count(), 9);
for (index, e) in expected.into_iter().enumerate() {
assert_eq!(encoder.current_index() as usize, index);
assert_eq!(encoder.next_part().unwrap(), e);
}
}
#[test]
fn test_ur_encoder_decoder_bc_crypto_request() {
fn crypto_seed() -> Vec<u8> {
let mut e = minicbor::Encoder::new(Vec::new());
let uuid = hex::decode("020C223A86F7464693FC650EF3CAC047").unwrap();
let seed_digest =
hex::decode("E824467CAFFEAF3BBC3E0CA095E660A9BAD80DDB6A919433A37161908B9A3986")
.unwrap();
e.map(2)
.unwrap()
.u8(1)
.unwrap()
.tag(minicbor::data::Tag::new(37))
.unwrap()
.bytes(&uuid)
.unwrap()
.u8(2)
.unwrap()
.tag(minicbor::data::Tag::new(500))
.unwrap()
.map(1)
.unwrap()
.u8(1)
.unwrap()
.tag(minicbor::data::Tag::new(600))
.unwrap()
.bytes(&seed_digest)
.unwrap();
e.into_writer()
}
let data = crypto_seed();
let encoded = encode(&data, &UrType::new("crypto-request").unwrap()).unwrap();
let expected = "ur:crypto-request/oeadtpdagdaobncpftlnylfgfgmuztihbawfsgrtflaotaadwkoyadtaaohdhdcxvsdkfgkepezepefrrffmbnnbmdvahnptrdtpbtuyimmemweootjshsmhlunyeslnameyhsdi";
assert_eq!(encoded, expected);
let decoded = decode(&encoded).unwrap();
assert_eq!((Kind::SinglePart, data), decoded);
}
#[test]
fn test_multipart_ur() {
let ur = make_message_ur(32767, "Wolf");
let mut encoder = Encoder::bytes(&ur, 1000).unwrap();
let mut decoder = Decoder::default();
while !decoder.complete() {
assert_eq!(decoder.message().unwrap(), None);
decoder.receive(&encoder.next_part().unwrap()).unwrap();
}
assert_eq!(decoder.message().unwrap(), Some(ur));
}
#[test]
fn test_data_encode() {
assert_eq!(
encode(b"data", &UrType::bytes()).unwrap(),
"ur:bytes/iehsjyhspmwfwfia"
);
}
#[test]
fn test_case_fold() {
let lower = encode(b"data", &UrType::bytes()).unwrap();
let upper = qr_string(&lower);
assert_eq!(decode(&upper).unwrap(), decode(&lower).unwrap());
}
#[test]
fn test_type_stickiness() {
let data = b"Ten chars!".repeat(5);
let mut enc_a = Encoder::new(&data, 5, &UrType::new("alpha").unwrap()).unwrap();
let mut enc_b = Encoder::new(&data, 5, &UrType::new("beta").unwrap()).unwrap();
let mut decoder = Decoder::default();
decoder.receive(&enc_a.next_part().unwrap()).unwrap();
assert!(matches!(
decoder.receive(&enc_b.next_part().unwrap()),
Err(Error::UnexpectedType { .. })
));
}
#[test]
fn test_invalid_scheme() {
assert!(matches!(
decode("uhr:bytes/aeadaolazmjendeoti"),
Err(Error::InvalidScheme)
));
}
#[test]
fn test_custom_encoder() {
let data = b"Ten chars!";
let mut encoder = Encoder::new(data, 5, &UrType::new("my-scheme").unwrap()).unwrap();
assert_eq!(
encoder.next_part().unwrap(),
"ur:my-scheme/1-2/lpadaobkcywkwmhfwnfeghihjtcxiansvomopr"
);
}
#[test]
fn test_not_multipart() {
let mut decoder = Decoder::default();
assert!(matches!(
decoder.receive("ur:bytes/iehsjyhspmwfwfia"),
Err(Error::NotMultiPart)
));
}
#[test]
fn test_bc_ur_example() {
let cbor = hex::decode("83010203").unwrap(); let ur = encode(&cbor, &UrType::new("test").unwrap()).unwrap();
assert_eq!(ur, "ur:test/lsadaoaxjygonesw");
let (kind, data) = decode(&ur).unwrap();
assert_eq!(kind, Kind::SinglePart);
assert_eq!(data, cbor);
}
#[test]
fn test_parse_and_decode_with_type() {
let ur = encode(b"data", &UrType::bytes()).unwrap();
let parsed = parse(&ur).unwrap();
assert_eq!(parsed.kind, Kind::SinglePart);
assert_eq!(parsed.ur_type.as_str(), "bytes");
assert!(parsed.indices.is_none());
let (ty, kind, payload) = decode_with_type(&ur).unwrap();
assert_eq!(ty.as_str(), "bytes");
assert_eq!(kind, Kind::SinglePart);
assert_eq!(payload, b"data");
}
#[test]
fn test_invalid_type_and_indices() {
assert!(matches!(UrType::new(""), Err(Error::InvalidType)));
assert!(matches!(UrType::new("Bad_Type"), Err(Error::InvalidType)));
assert!(matches!(
parse("ur:bytes/0-1/aeadaolazmjendeoti"),
Err(Error::InvalidIndices)
));
assert!(matches!(
parse("ur:bytes/1-0/aeadaolazmjendeoti"),
Err(Error::InvalidIndices)
));
assert!(matches!(
parse("ur:bytes/foo/aeadaolazmjendeoti"),
Err(Error::InvalidIndices)
));
}
#[test]
fn test_expected_type_and_uri_limit() {
let data = b"Ten chars!".repeat(5);
let mut enc = Encoder::new(&data, 5, &UrType::new("alpha").unwrap()).unwrap();
let part = enc.next_part().unwrap();
let mut decoder = Decoder::default().with_expected_type(UrType::new("beta").unwrap());
assert!(matches!(
decoder.receive(&part),
Err(Error::UnexpectedType { .. })
));
let limits = DecoderLimits {
max_uri_len: 8,
..DecoderLimits::default()
};
let mut short = Decoder::with_limits(limits);
assert!(matches!(
short.receive(&part),
Err(Error::ResourceLimit("uri_len"))
));
}
#[test]
fn test_multipart_index_mismatch() {
let data = b"Ten chars!".repeat(5);
let mut enc = Encoder::bytes(&data, 5).unwrap();
let part = enc.next_part().unwrap();
let corrupted = part.replacen("/1-", "/2-", 1);
let mut decoder = Decoder::default();
assert!(matches!(
decoder.receive(&corrupted),
Err(Error::InvalidIndices)
));
}
#[test]
fn test_empty_single_part() {
let ur = encode(&[], &UrType::bytes()).unwrap();
let (kind, payload) = decode(&ur).unwrap();
assert_eq!(kind, Kind::SinglePart);
assert!(payload.is_empty());
}
#[test]
fn test_parse_normalized_body_is_lowercase() {
let parsed = parse_normalized("ur:bytes/IEHSJYHSPMWFWFIA").unwrap();
assert_eq!(parsed.body, "iehsjyhspmwfwfia");
assert_eq!(parsed.ur_type.as_str(), "bytes");
}
#[test]
fn test_uri_len_resource_limit_poisons() {
let data = b"Ten chars!".repeat(5);
let mut enc = Encoder::bytes(&data, 5).unwrap();
let part = enc.next_part().unwrap();
let limits = DecoderLimits {
max_uri_len: 8,
..DecoderLimits::default()
};
let mut decoder = Decoder::with_limits(limits);
assert!(matches!(
decoder.receive(&part),
Err(Error::ResourceLimit("uri_len"))
));
assert!(decoder.is_poisoned());
assert!(matches!(
decoder.receive(&part),
Err(Error::ResourceLimit("uri_len"))
));
assert!(matches!(
decoder.message(),
Err(Error::ResourceLimit("uri_len"))
));
}
#[test]
fn test_decoder_progress_accessors() {
let ur = make_message_ur(256, "Wolf");
let mut encoder = Encoder::bytes(&ur, 30).unwrap();
let mut decoder = Decoder::default();
assert_eq!(decoder.resolved_fragment_count(), None);
assert_eq!(decoder.fragment_count(), 0);
decoder.receive(&encoder.next_part().unwrap()).unwrap();
assert_eq!(decoder.resolved_fragment_count(), Some(1));
assert_eq!(decoder.fragment_count(), encoder.fragment_count() as usize);
let mut prev = 1;
while !decoder.complete() {
decoder.receive(&encoder.next_part().unwrap()).unwrap();
let now = decoder.resolved_fragment_count().unwrap();
assert!(now >= prev);
assert!(now <= decoder.fragment_count());
prev = now;
}
assert_eq!(
decoder.resolved_fragment_count(),
Some(decoder.fragment_count())
);
}
}