depolymerization

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

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 }