lib.rs (9304B)
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 //! uri-pack is an efficient binary format for URI 18 //! 19 //! ## Format 20 //! 21 //! Most commonly used characters (a-z . / - %) are encoded using 5b, remaining 22 //! ascii characters are encoded using 11b.If more than half the characters in an 23 //! uri are encoded with 5b, the encoded size is smaller than a simple ascii 24 //! format. 25 //! 26 //! On the majestic_million database, 98.77% of the domain name where smaller, 27 //! going from an average of 14b to an average of 10b. 28 //! 29 //! ## Usage 30 //! 31 //! ``` rust 32 //! use uri_pack::{pack_uri, unpack_uri}; 33 //! 34 //! let domain = "http://example.com/static_file/image.png"; 35 //! let encoded = pack_uri(domain).unwrap(); 36 //! let decoded = unpack_uri(&encoded).unwrap(); 37 //! assert_eq!(domain, decoded); 38 //! ``` 39 //! 40 41 /// Pack an URI ascii char 42 /// Panic if char not supported 43 fn pack_ascii(c: u8) -> u8 { 44 [ 45 67, 68, 69, 70, 29, 71, 72, 73, 74, 75, 76, 77, 28, 26, 27, 57, 58, 59, 60, 61, 62, 63, 64, 46 65, 66, 78, 79, 80, 81, 82, 83, 84, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 47 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 85, 86, 87, 88, 30, 89, 0, 1, 2, 3, 4, 5, 48 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 90, 91, 92, 93, 49 ][(c - b'!') as usize] 50 } 51 52 /// Unpack an URI ascii char 53 /// Panic if char not supported 54 fn unpack_ascii(c: u8) -> u8 { 55 #[allow(clippy::byte_char_slices)] 56 [ 57 b'a', b'b', b'c', b'd', b'e', b'f', b'g', b'h', b'i', b'j', b'k', b'l', b'm', b'n', b'o', 58 b'p', b'q', b'r', b's', b't', b'u', b'v', b'w', b'x', b'y', b'z', b'.', b'/', b'-', b'%', 59 b'_', b'A', b'B', b'C', b'D', b'E', b'F', b'G', b'H', b'I', b'J', b'K', b'L', b'M', b'N', 60 b'O', b'P', b'Q', b'R', b'S', b'T', b'U', b'V', b'W', b'X', b'Y', b'Z', b'0', b'1', b'2', 61 b'3', b'4', b'5', b'6', b'7', b'8', b'9', b'!', b'"', b'#', b'$', b'&', b'\'', b'(', b')', 62 b'*', b'+', b',', b':', b';', b'<', b'=', b'>', b'?', b'@', b'[', b'\\', b']', b'^', b'`', 63 b'{', b'|', b'}', b'~', 64 ][c as usize] 65 } 66 67 /// Check if an ascii char is supported by the encoding 68 fn supported_ascii(c: &u8) -> bool { 69 (b'!'..=b'~').contains(c) 70 } 71 72 /// Extended packing limit 73 const EXTENDED: u8 = 30; 74 /// EOF u5 encoding 75 const TERMINATOR: u8 = 31; 76 77 #[derive(Debug, Clone, Copy, thiserror::Error)] 78 pub enum EncodeErr { 79 #[error("{0} is not a valid uri char")] 80 UnsupportedChar(u8), 81 } 82 83 #[derive(Debug, Clone, Copy, thiserror::Error)] 84 pub enum DecodeErr { 85 #[error("An extended encoded char have been passed as an simple one")] 86 ExpectedExtended, 87 #[error("{0} is not an simple encoded char")] 88 UnexpectedSimpleChar(u8), 89 #[error("{0} is not an extended encoded char")] 90 UnexpectedExtendedChar(u8), 91 #[error("Missing bits")] 92 UnexpectedEOF, 93 } 94 95 /// Pack an uri string into an optimized binary format 96 pub fn pack_uri(uri: &str) -> Result<Vec<u8>, EncodeErr> { 97 let len = uri.len(); 98 let mut vec = Vec::with_capacity(len); 99 100 if let Some(char) = uri.as_bytes().iter().find(|c| !supported_ascii(c)) { 101 return Err(EncodeErr::UnsupportedChar(*char)); 102 } 103 104 // Holds [buff_bits] pending bits beginning from the most significant bits 105 let (mut buff, mut buff_bits) = (0u8, 0u8); 106 107 // Write [nb_bits] less significant bits from [nb] to [buff] 108 let mut write_bits = |nb: u8, mut nb_bits: u8| { 109 while nb_bits > 0 { 110 // Amount of bits we can write in buffer 111 let writable = (8 - buff_bits).min(nb_bits); 112 // Remove non writable bits 113 let rmv_right = nb >> (nb_bits - writable); 114 let rmv_left = rmv_right << (8 - writable); 115 // Align remaining bits with buff blank bits 116 let align = rmv_left >> buff_bits; 117 118 // Write bits in buffer 119 buff |= align; 120 buff_bits += writable; 121 nb_bits -= writable; 122 123 // Store buffer if full 124 if buff_bits == 8 { 125 vec.push(buff); 126 buff = 0; 127 buff_bits = 0; 128 } 129 } 130 }; 131 132 for c in uri.bytes() { 133 let nb = pack_ascii(c); 134 if nb < EXTENDED { 135 write_bits(nb, 5) 136 } else { 137 write_bits(EXTENDED, 5); 138 write_bits(nb - EXTENDED, 6); 139 } 140 } 141 write_bits(TERMINATOR, 5); 142 143 // Push pending buffer if not empty 144 if buff_bits > 0 { 145 vec.push(buff); 146 } 147 148 Ok(vec) 149 } 150 151 /// Unpack an uri string from its optimized binary format 152 pub fn unpack_uri(bytes: &[u8]) -> Result<String, DecodeErr> { 153 let mut buf = String::with_capacity(bytes.len()); 154 let mut iter = bytes.iter(); 155 156 // Holds [buff_bits] pending bits beginning from the most significant bits 157 let (mut buff, mut buff_bits) = (0u8, 0u8); 158 159 // Write [nb_bits] less significant bits from [buff] to [nb] 160 let mut read_nb = |mut nb_bits: u8| -> Result<u8, DecodeErr> { 161 let mut nb = 0; 162 while nb_bits > 0 { 163 // Load buff if empty 164 if buff_bits == 0 { 165 buff = *iter.next().ok_or(DecodeErr::UnexpectedEOF)?; 166 buff_bits = 8; 167 } 168 // Amount of bits we can read from buff 169 let readable = buff_bits.min(nb_bits); 170 // Remove non writable bits 171 let rmv_left = buff << (8 - buff_bits); 172 // Align remaining bits with nb blank bits 173 let align = rmv_left >> (8 - readable); 174 // Read bits from buff 175 nb = (nb << readable) | align; 176 buff_bits -= readable; 177 nb_bits -= readable; 178 } 179 Ok(nb) 180 }; 181 182 loop { 183 let encoded = match read_nb(5)? { 184 TERMINATOR => break, 185 EXTENDED => read_nb(6)? + EXTENDED, 186 nb => nb, 187 }; 188 buf.push(unpack_ascii(encoded) as char); 189 } 190 191 Ok(buf) 192 } 193 194 #[cfg(test)] 195 #[macro_use(quickcheck)] 196 extern crate quickcheck_macros; 197 198 #[cfg(test)] 199 mod test { 200 use std::str::FromStr; 201 202 use serde_json::Value; 203 204 use crate::{EXTENDED, pack_ascii, pack_uri, supported_ascii, unpack_ascii, unpack_uri}; 205 206 /// Ascii char that can be packed into 5 bits 207 const PACKED: [u8; 30] = [ 208 b'a', b'b', b'c', b'd', b'e', b'f', b'g', b'h', b'i', b'j', b'k', b'l', b'm', b'n', b'o', 209 b'p', b'q', b'r', b's', b't', b'u', b'v', b'w', b'x', b'y', b'z', b'.', b'/', b'-', b'%', 210 ]; 211 212 #[test] 213 /// Check support every packable ascii character is packed 214 fn packed() { 215 for c in PACKED { 216 assert!(pack_ascii(c) < EXTENDED); 217 } 218 } 219 220 #[test] 221 /// Check support every ascii graphic character and space 222 fn supported() { 223 for c in (0..=255u8).filter(supported_ascii) { 224 assert_eq!(unpack_ascii(pack_ascii(c)), c); 225 } 226 } 227 228 #[test] 229 /// Check error on unsupported char 230 fn unsupported() { 231 for c in (0..=255u8).filter(|c| !supported_ascii(c)) { 232 let string = String::from(c as char); 233 assert!(pack_uri(&string).is_err()); 234 } 235 } 236 237 #[test] 238 fn url_simple() { 239 let mut majestic = 240 csv::Reader::from_reader(include_str!("majestic_million.csv").as_bytes()); 241 for record in majestic.records() { 242 let domain = &record.unwrap()[2]; 243 let encoded = pack_uri(domain).unwrap(); 244 let decoded = unpack_uri(&encoded).unwrap(); 245 assert_eq!(domain, decoded); 246 } 247 } 248 249 #[test] 250 fn url_complex() { 251 let mut json = Value::from_str(include_str!("urltestdata.json")) 252 .expect("JSON parse error in urltestdata.json"); 253 for entry in json.as_array_mut().unwrap() { 254 if entry.is_string() { 255 continue; // ignore comments 256 } 257 258 let href = entry.get("href").and_then(|it| it.as_str()).unwrap_or(""); 259 if href.chars().any(|c| !c.is_ascii_graphic() || c != ' ') { 260 continue; // extended ascii 261 } 262 let encoded = pack_uri(href).unwrap_or_else(|_| panic!("Failed to encode {}", &href)); 263 let decoded = unpack_uri(&encoded) 264 .unwrap_or_else(|_| panic!("Failed to decode encoded {}", &href)); 265 assert_eq!(href, decoded); 266 } 267 } 268 269 #[quickcheck] 270 fn fuzz(input: String) -> bool { 271 if input.as_bytes().iter().all(supported_ascii) { 272 let packed = pack_uri(&input).unwrap(); 273 let unpacked = unpack_uri(&packed).unwrap(); 274 input == unpacked 275 } else { 276 true 277 } 278 } 279 }