depolymerization

wire gateway for Bitcoin/Ethereum
Log | Files | Refs | Submodules | README | LICENSE

metadata.rs (7551B)


      1 /*
      2   This file is part of TALER
      3   Copyright (C) 2022-2025, 2026 Taler Systems SA
      4 
      5   TALER is free software; you can redistribute it and/or modify it under the
      6   terms of the GNU Affero General Public License as published by the Free Software
      7   Foundation; either version 3, or (at your option) any later version.
      8 
      9   TALER is distributed in the hope that it will be useful, but WITHOUT ANY
     10   WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
     11   A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more details.
     12 
     13   You should have received a copy of the GNU Affero General Public License along with
     14   TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
     15 */
     16 
     17 use std::fmt::Debug;
     18 
     19 use compact_str::CompactString;
     20 use taler_common::api::{EddsaPublicKey, ShortHashCode};
     21 use url::Url;
     22 
     23 #[derive(Debug, Clone, thiserror::Error)]
     24 pub enum DecodeErr {
     25     #[error("Unknown first byte: {0}")]
     26     UnknownFirstByte(u8),
     27     #[error(transparent)]
     28     UriPack(#[from] uri_pack::DecodeErr),
     29     #[error(transparent)]
     30     Metadata(#[from] std::str::Utf8Error),
     31     #[error("Unexpected end of file")]
     32     UnexpectedEOF,
     33 }
     34 
     35 #[derive(Debug, Clone, thiserror::Error)]
     36 pub enum EncodeErr {
     37     #[error("Unsupported URI scheme {0}")]
     38     UnsupportedScheme(String),
     39 }
     40 
     41 /// Encoded metadata for outgoing transaction
     42 #[derive(Debug, Clone, PartialEq, Eq)]
     43 pub enum OutMetadata {
     44     Debit {
     45         wtid: ShortHashCode,
     46         url: Url,
     47         metadata: Option<CompactString>,
     48     },
     49     Bounce {
     50         bounced: [u8; 32],
     51     },
     52 }
     53 
     54 // We leave a potential special meaning for u8::MAX
     55 const BOUNCE_BYTE: u8 = 0b11111110;
     56 const METADATA_FLAG: u8 = 0b10000000;
     57 
     58 impl OutMetadata {
     59     pub fn encode(&self) -> Result<Vec<u8>, EncodeErr> {
     60         let mut buffer = Vec::new();
     61         match self {
     62             OutMetadata::Debit {
     63                 wtid,
     64                 url,
     65                 metadata,
     66             } => {
     67                 let mut scheme_id = match url.scheme() {
     68                     "https" => 0,
     69                     "http" => 1,
     70                     scheme => return Err(EncodeErr::UnsupportedScheme(scheme.to_string())),
     71                 };
     72                 if metadata.is_some() {
     73                     scheme_id |= METADATA_FLAG;
     74                 }
     75                 buffer.push(scheme_id);
     76                 buffer.extend_from_slice(wtid.as_slice());
     77                 if let Some(metadata) = metadata {
     78                     buffer.push(metadata.len() as u8);
     79                     buffer.extend_from_slice(metadata.as_bytes());
     80                 }
     81                 let parts = format!("{}{}", url.domain().unwrap_or(""), url.path());
     82                 let packed = uri_pack::pack_uri(&parts).unwrap();
     83                 buffer.extend_from_slice(&packed);
     84             }
     85             OutMetadata::Bounce { bounced } => {
     86                 buffer.push(BOUNCE_BYTE);
     87                 buffer.extend_from_slice(bounced.as_ref());
     88             }
     89         }
     90         Ok(buffer)
     91     }
     92 
     93     pub fn decode(bytes: &[u8]) -> Result<Self, DecodeErr> {
     94         if bytes.is_empty() {
     95             return Err(DecodeErr::UnexpectedEOF);
     96         }
     97         let id = bytes[0];
     98         if id == BOUNCE_BYTE {
     99             if bytes.len() < 33 {
    100                 return Err(DecodeErr::UnexpectedEOF);
    101             }
    102             Ok(OutMetadata::Bounce {
    103                 bounced: bytes[1..33].try_into().unwrap(),
    104             })
    105         } else {
    106             let scheme_id = id & !METADATA_FLAG;
    107             if bytes.len() < 33 {
    108                 return Err(DecodeErr::UnexpectedEOF);
    109             }
    110             let scheme = match scheme_id {
    111                 0 => "https",
    112                 1 => "http",
    113                 _ => return Err(DecodeErr::UnknownFirstByte(id)),
    114             };
    115             let (metadata, uri) = if (id & METADATA_FLAG) != 0 {
    116                 let len = bytes[33] as usize;
    117                 let metadata = CompactString::from_utf8(&bytes[33 + 1..][..len])?;
    118                 (Some(metadata), (33 + len + 1)..)
    119             } else {
    120                 (None, 33..)
    121             };
    122             let packed = format!("{}://{}", scheme, uri_pack::unpack_uri(&bytes[uri])?,);
    123             let url = Url::parse(&packed).unwrap();
    124             Ok(OutMetadata::Debit {
    125                 wtid: bytes[1..33].try_into().unwrap(),
    126                 url,
    127                 metadata,
    128             })
    129         }
    130     }
    131 }
    132 
    133 /// Encoded metadata for incoming transaction
    134 #[derive(Debug, Clone, PartialEq, Eq)]
    135 pub enum InMetadata {
    136     Credit { reserve_pub: EddsaPublicKey },
    137 }
    138 
    139 impl InMetadata {
    140     pub fn encode(&self) -> Vec<u8> {
    141         let mut buffer = Vec::new();
    142         match self {
    143             InMetadata::Credit { reserve_pub } => {
    144                 buffer.push(0);
    145                 buffer.extend_from_slice(reserve_pub.as_slice());
    146             }
    147         }
    148         buffer
    149     }
    150 
    151     pub fn decode(bytes: &[u8]) -> Result<Self, DecodeErr> {
    152         if bytes.is_empty() {
    153             return Err(DecodeErr::UnexpectedEOF);
    154         }
    155         match bytes[0] {
    156             0 => {
    157                 if bytes.len() < 33 {
    158                     return Err(DecodeErr::UnexpectedEOF);
    159                 }
    160                 Ok(InMetadata::Credit {
    161                     reserve_pub: bytes[1..33].try_into().unwrap(),
    162                 })
    163             }
    164             unknown => Err(DecodeErr::UnknownFirstByte(unknown)),
    165         }
    166     }
    167 }
    168 
    169 #[cfg(test)]
    170 mod test {
    171 
    172     use taler_common::api::{EddsaPublicKey, ShortHashCode};
    173     use url::Url;
    174 
    175     use crate::{
    176         metadata::{InMetadata, OutMetadata},
    177         rand_slice,
    178     };
    179 
    180     #[test]
    181     fn decode_encode_credit() {
    182         for _ in 0..4 {
    183             let metadata = InMetadata::Credit {
    184                 reserve_pub: EddsaPublicKey::rand(),
    185             };
    186             let encoded = metadata.encode();
    187             let decoded = InMetadata::decode(&encoded).unwrap();
    188             assert_eq!(decoded, metadata);
    189         }
    190     }
    191 
    192     #[test]
    193     fn decode_encode_debit() {
    194         let urls = [
    195             "https://git.taler.net/",
    196             "https://git.taler.net/depolymerization.git/",
    197             "http://git.taler.net/",
    198             "http://git.taler.net/depolymerization.git/",
    199         ];
    200         let metadatas = [None, Some("REFERENCE"), Some("internal-id")];
    201         for url in urls {
    202             for metadata in metadatas {
    203                 let wtid = ShortHashCode::rand();
    204                 let url = Url::parse(url).unwrap();
    205                 let metadata = OutMetadata::Debit {
    206                     wtid,
    207                     url,
    208                     metadata: metadata.map(|it| it.into()),
    209                 };
    210                 let encoded = metadata.encode().unwrap();
    211                 let decoded = OutMetadata::decode(&encoded).unwrap();
    212                 assert_eq!(decoded, metadata);
    213             }
    214         }
    215     }
    216 
    217     #[test]
    218     fn encode_unknown_scheme() {
    219         let url = "https+wtf://git.taler.net";
    220         let url = Url::parse(url).unwrap();
    221         let metadata = OutMetadata::Debit {
    222             wtid: ShortHashCode::rand(),
    223             url,
    224             metadata: None,
    225         };
    226         let encoded = metadata.encode();
    227         assert!(encoded.is_err())
    228     }
    229 
    230     #[test]
    231     fn decode_encode_bounce() {
    232         for _ in 0..4 {
    233             let id: [u8; 32] = rand_slice();
    234             let info = OutMetadata::Bounce { bounced: id };
    235             let encoded = info.encode().unwrap();
    236             let decoded = OutMetadata::decode(&encoded).unwrap();
    237             assert_eq!(decoded, info);
    238         }
    239     }
    240 }