#[derive(Debug)]
pub enum Error {
Bytewords(crate::bytewords::Error),
Fountain(crate::fountain::Error),
InvalidScheme,
TypeUnspecified,
InvalidCharacters,
InvalidIndices,
NotMultiPart,
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::Bytewords(e) => write!(f, "{e}"),
Error::Fountain(e) => write!(f, "{e}"),
Error::InvalidScheme => write!(f, "Invalid scheme"),
Error::TypeUnspecified => write!(f, "No type specified"),
Error::InvalidCharacters => write!(f, "Type contains invalid characters"),
Error::InvalidIndices => write!(f, "Invalid indices"),
Error::NotMultiPart => write!(f, "Can't decode single-part UR as multi-part"),
}
}
}
impl From<crate::bytewords::Error> for Error {
fn from(e: crate::bytewords::Error) -> Self {
Self::Bytewords(e)
}
}
impl From<crate::fountain::Error> for Error {
fn from(e: crate::fountain::Error) -> Self {
Self::Fountain(e)
}
}
pub fn encode<T: Into<String>>(data: &[u8], ur_type: T) -> String {
let body = crate::bytewords::encode(data, crate::bytewords::Style::Minimal);
encode_ur(&[ur_type.into(), body])
}
#[must_use]
fn encode_ur(items: &[String]) -> String {
format!("{}:{}", "ur", items.join("/"))
}
pub struct Encoder {
fountain: crate::fountain::Encoder,
ur_type: String,
}
impl Encoder {
pub fn new<T: Into<String>>(
message: &[u8],
max_fragment_length: usize,
ur_type: T,
) -> Result<Self, Error> {
Ok(Self {
fountain: crate::fountain::Encoder::new(message, max_fragment_length)?,
ur_type: ur_type.into(),
})
}
pub fn next_part(&mut self) -> Result<String, Error> {
let part = self.fountain.next_part();
let body = crate::bytewords::encode(&part.cbor()?, crate::bytewords::Style::Minimal);
Ok(encode_ur(&[self.ur_type.clone(), part.sequence_id(), body]))
}
#[must_use]
pub fn current_index(&self) -> usize {
self.fountain.current_sequence()
}
#[must_use]
pub fn fragment_count(&self) -> usize {
self.fountain.fragment_count()
}
}
#[derive(Debug, PartialEq, Eq)]
pub enum Kind {
SinglePart,
MultiPart,
}
pub fn decode(value: &str) -> Result<(Kind, Vec<u8>), Error> {
let strip_scheme = value.strip_prefix("ur:").ok_or(Error::InvalidScheme)?;
let (type_, strip_type) = strip_scheme.split_once('/').ok_or(Error::TypeUnspecified)?;
if !type_
.trim_start_matches(|c: char| c.is_ascii_alphanumeric() || c == '-')
.is_empty()
{
return Err(Error::InvalidCharacters);
}
match strip_type.rsplit_once('/') {
None => Ok((
Kind::SinglePart,
crate::bytewords::decode(strip_type, crate::bytewords::Style::Minimal)?,
)),
Some((indices, payload)) => {
let (idx, idx_total) = indices.split_once('-').ok_or(Error::InvalidIndices)?;
if idx.parse::<u16>().is_err() || idx_total.parse::<u16>().is_err() {
return Err(Error::InvalidIndices);
}
Ok((
Kind::MultiPart,
crate::bytewords::decode(payload, crate::bytewords::Style::Minimal)?,
))
}
}
}
#[derive(Default)]
pub struct Decoder {
fountain: crate::fountain::Decoder,
}
impl Decoder {
pub fn receive(&mut self, value: &str) -> Result<(), Error> {
let (kind, decoded) = decode(value)?;
if kind != Kind::MultiPart {
return Err(Error::NotMultiPart);
}
self.fountain
.receive(crate::fountain::Part::from_cbor(decoded.as_slice())?)?;
Ok(())
}
#[must_use]
pub fn complete(&self) -> bool {
self.fountain.complete()
}
pub fn message(&self) -> Result<Option<Vec<u8>>, Error> {
self.fountain.message().map_err(Error::from)
}
}
#[cfg(test)]
mod tests {
use super::*;
use minicbor::{bytes::ByteVec, data::Tag};
fn make_message_ur(length: usize, seed: &str) -> Vec<u8> {
let message = crate::xoshiro::test_utils::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, "bytes");
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::new(&ur, 30, "bytes").unwrap();
let expected = vec![
"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(), index);
assert_eq!(encoder.next_part().unwrap(), e);
}
}
#[test]
fn test_ur_encoder_decoder_bc_crypto_request() {
fn crypto_seed() -> Result<Vec<u8>, minicbor::encode::Error<std::convert::Infallible>> {
let mut e = minicbor::Encoder::new(Vec::new());
let uuid = hex::decode("020C223A86F7464693FC650EF3CAC047").unwrap();
let seed_digest =
hex::decode("E824467CAFFEAF3BBC3E0CA095E660A9BAD80DDB6A919433A37161908B9A3986")
.unwrap();
#[rustfmt::skip]
e.map(2)?
.u8(1)?.tag(Tag::Unassigned(37))?.bytes(&uuid)?
.u8(2)?.tag(Tag::Unassigned(500))?.map(1)?
.u8(1)?.tag(Tag::Unassigned(600))?.bytes(&seed_digest)?;
Ok(e.into_writer())
}
let data = crypto_seed().unwrap();
let e = encode(&data, "crypto-request");
let expected = "ur:crypto-request/oeadtpdagdaobncpftlnylfgfgmuztihbawfsgrtflaotaadwkoyadtaaohdhdcxvsdkfgkepezepefrrffmbnnbmdvahnptrdtpbtuyimmemweootjshsmhlunyeslnameyhsdi";
assert_eq!(expected, e);
let decoded = decode(e.as_str()).unwrap();
assert_eq!((Kind::SinglePart, data), decoded);
}
#[test]
fn test_multipart_ur() {
let ur = make_message_ur(32767, "Wolf");
let mut encoder = Encoder::new(&ur, 1000, "bytes").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_decoder() {
assert!(matches!(
decode("uhr:bytes/aeadaolazmjendeoti"),
Err(Error::InvalidScheme)
));
assert!(matches!(
decode("ur:aeadaolazmjendeoti"),
Err(Error::TypeUnspecified)
));
assert!(matches!(
decode("ur:bytes#4/aeadaolazmjendeoti"),
Err(Error::InvalidCharacters)
));
assert!(matches!(
decode("ur:bytes/1-1a/aeadaolazmjendeoti"),
Err(Error::InvalidIndices)
));
assert!(matches!(
decode("ur:bytes/1-1/toomuch/aeadaolazmjendeoti"),
Err(Error::InvalidIndices)
));
decode("ur:bytes/aeadaolazmjendeoti").unwrap();
decode("ur:whatever-12/aeadaolazmjendeoti").unwrap();
}
}