metadata.rs (6441B)
1 /* 2 This file is part of TALER 3 Copyright (C) 2022-2025 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 taler_common::api_common::{EddsaPublicKey, ShortHashCode}; 20 use url::Url; 21 22 #[derive(Debug, Clone, thiserror::Error)] 23 pub enum DecodeErr { 24 #[error("Unknown first byte: {0}")] 25 UnknownFirstByte(u8), 26 #[error(transparent)] 27 UriPack(#[from] uri_pack::DecodeErr), 28 #[error("Unexpected end of file")] 29 UnexpectedEOF, 30 } 31 32 #[derive(Debug, Clone, thiserror::Error)] 33 pub enum EncodeErr { 34 #[error("Unsupported URI scheme {0}")] 35 UnsupportedScheme(String), 36 } 37 38 /// Encoded metadata for outgoing transaction 39 #[derive(Debug, Clone, PartialEq, Eq)] 40 pub enum OutMetadata { 41 Debit { wtid: ShortHashCode, url: Url }, 42 Bounce { bounced: [u8; 32] }, 43 } 44 45 // We leave a potential special meaning for u8::MAX 46 const BOUNCE_BYTE: u8 = u8::MAX - 1; 47 48 impl OutMetadata { 49 pub fn encode(&self) -> Result<Vec<u8>, EncodeErr> { 50 let mut buffer = Vec::new(); 51 match self { 52 OutMetadata::Debit { wtid, url } => { 53 let scheme_id = match url.scheme() { 54 "https" => 0, 55 "http" => 1, 56 scheme => return Err(EncodeErr::UnsupportedScheme(scheme.to_string())), 57 }; 58 buffer.push(scheme_id); 59 buffer.extend_from_slice(wtid.as_slice()); 60 let parts = format!("{}{}", url.domain().unwrap_or(""), url.path()); 61 let packed = uri_pack::pack_uri(&parts).unwrap(); 62 buffer.extend_from_slice(&packed); 63 } 64 OutMetadata::Bounce { bounced } => { 65 buffer.push(BOUNCE_BYTE); 66 buffer.extend_from_slice(bounced.as_ref()); 67 } 68 } 69 Ok(buffer) 70 } 71 72 pub fn decode(bytes: &[u8]) -> Result<Self, DecodeErr> { 73 if bytes.is_empty() { 74 return Err(DecodeErr::UnexpectedEOF); 75 } 76 match bytes[0] { 77 0..=1 => { 78 if bytes.len() < 33 { 79 return Err(DecodeErr::UnexpectedEOF); 80 } 81 let scheme = match bytes[0] { 82 0 => "https", 83 1 => "http", 84 _ => unreachable!(), 85 }; 86 let packed = format!("{}://{}", scheme, uri_pack::unpack_uri(&bytes[33..])?,); 87 let url = Url::parse(&packed).unwrap(); 88 Ok(OutMetadata::Debit { 89 wtid: bytes[1..33].try_into().unwrap(), 90 url, 91 }) 92 } 93 BOUNCE_BYTE => { 94 if bytes.len() < 33 { 95 return Err(DecodeErr::UnexpectedEOF); 96 } 97 Ok(OutMetadata::Bounce { 98 bounced: bytes[1..33].try_into().unwrap(), 99 }) 100 } 101 unknown => Err(DecodeErr::UnknownFirstByte(unknown)), 102 } 103 } 104 } 105 106 /// Encoded metadata for incoming transaction 107 #[derive(Debug, Clone, PartialEq, Eq)] 108 pub enum InMetadata { 109 Credit { reserve_pub: EddsaPublicKey }, 110 } 111 112 impl InMetadata { 113 pub fn encode(&self) -> Vec<u8> { 114 let mut buffer = Vec::new(); 115 match self { 116 InMetadata::Credit { reserve_pub } => { 117 buffer.push(0); 118 buffer.extend_from_slice(reserve_pub.as_slice()); 119 } 120 } 121 buffer 122 } 123 124 pub fn decode(bytes: &[u8]) -> Result<Self, DecodeErr> { 125 if bytes.is_empty() { 126 return Err(DecodeErr::UnexpectedEOF); 127 } 128 match bytes[0] { 129 0 => { 130 if bytes.len() < 33 { 131 return Err(DecodeErr::UnexpectedEOF); 132 } 133 Ok(InMetadata::Credit { 134 reserve_pub: bytes[1..33].try_into().unwrap(), 135 }) 136 } 137 unknown => Err(DecodeErr::UnknownFirstByte(unknown)), 138 } 139 } 140 } 141 142 #[cfg(test)] 143 mod test { 144 145 use taler_common::api_common::{EddsaPublicKey, ShortHashCode}; 146 use url::Url; 147 148 use crate::{ 149 metadata::{InMetadata, OutMetadata}, 150 rand_slice, 151 }; 152 153 #[test] 154 fn decode_encode_credit() { 155 for _ in 0..4 { 156 let metadata = InMetadata::Credit { 157 reserve_pub: EddsaPublicKey::rand(), 158 }; 159 let encoded = metadata.encode(); 160 let decoded = InMetadata::decode(&encoded).unwrap(); 161 assert_eq!(decoded, metadata); 162 } 163 } 164 165 #[test] 166 fn decode_encode_debit() { 167 let urls = [ 168 "https://git.taler.net/", 169 "https://git.taler.net/depolymerization.git/", 170 "http://git.taler.net/", 171 "http://git.taler.net/depolymerization.git/", 172 ]; 173 for url in urls { 174 let wtid = ShortHashCode::rand(); 175 let url = Url::parse(url).unwrap(); 176 let metadata = OutMetadata::Debit { wtid, url }; 177 let encoded = metadata.encode().unwrap(); 178 let decoded = OutMetadata::decode(&encoded).unwrap(); 179 assert_eq!(decoded, metadata); 180 } 181 } 182 183 #[test] 184 fn encode_unknown_scheme() { 185 let url = "https+wtf://git.taler.net"; 186 let url = Url::parse(url).unwrap(); 187 let metadata = OutMetadata::Debit { 188 wtid: ShortHashCode::rand(), 189 url, 190 }; 191 let encoded = metadata.encode(); 192 assert!(encoded.is_err()) 193 } 194 195 #[test] 196 fn decode_encode_bounce() { 197 for _ in 0..4 { 198 let id: [u8; 32] = rand_slice(); 199 let info = OutMetadata::Bounce { bounced: id }; 200 let encoded = info.encode().unwrap(); 201 let decoded = OutMetadata::decode(&encoded).unwrap(); 202 assert_eq!(decoded, info); 203 } 204 } 205 }