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 }