├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md └── src └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "urlencoding" 3 | version = "1.0.0" 4 | authors = ["Bertram Truong ", "Kornel "] 5 | license = "MIT" 6 | description = "A Rust library for doing URL percentage encoding." 7 | repository = "https://github.com/bt/rust_urlencoding" 8 | keywords = ["url", "encoding", "urlencoding"] 9 | edition = "2018" 10 | categories = ["encoding", "web-programming"] 11 | 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Bertram Truong 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # urlencoding 2 | 3 | [![Latest Version](https://img.shields.io/crates/v/urlencoding.svg)](https://crates.io/crates/urlencoding) 4 | 5 | A Rust library for doing URL percentage encoding. 6 | 7 | Installation 8 | ============ 9 | 10 | This crate can be downloaded through Cargo. To do so, add the following line to your `Cargo.toml` file, under `dependencies`: 11 | 12 | ```toml 13 | urlencoding = "1.0.0" 14 | ``` 15 | 16 | Usage 17 | ===== 18 | 19 | To encode a string, do the following: 20 | 21 | ```rust 22 | extern crate urlencoding; 23 | 24 | use urlencoding::encode; 25 | 26 | fn main() { 27 | let encoded = encode("This string will be URL encoded."); 28 | println!("{}", encoded); 29 | // This%20string%20will%20be%20URL%20encoded. 30 | } 31 | ``` 32 | 33 | To decode a string, it's only slightly different: 34 | 35 | ```rust 36 | extern crate urlencoding; 37 | 38 | use urlencoding::decode; 39 | 40 | fn main() { 41 | let decoded = decode("%F0%9F%91%BE%20Exterminate%21"); 42 | println!("{}", decoded.unwrap()); 43 | // 👾 Exterminate! 44 | } 45 | ``` 46 | 47 | License 48 | ======= 49 | 50 | This project is licensed under the MIT license, Copyright (c) 2017 Bertram Truong. For more information see the `LICENSE` file. 51 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::str; 2 | use std::string::FromUtf8Error; 3 | use std::error::Error; 4 | use std::fmt::{self, Display}; 5 | use std::io::Write; 6 | use std::io; 7 | 8 | pub fn encode(data: &str) -> String { 9 | let mut escaped = Vec::with_capacity(data.len()); 10 | encode_into(data, &mut escaped).unwrap(); 11 | // Encoded string is guaranteed to be ASCII 12 | unsafe { 13 | String::from_utf8_unchecked(escaped) 14 | } 15 | } 16 | 17 | #[inline] 18 | fn encode_into(data: &str, mut escaped: W) -> io::Result<()> { 19 | for byte in data.as_bytes().iter() { 20 | match *byte { 21 | b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z' | b'-' | b'.' | b'_' | b'~' => { 22 | escaped.write(std::slice::from_ref(byte))?; 23 | }, 24 | other => { 25 | escaped.write(&[b'%', to_hex_digit(other >> 4), to_hex_digit(other & 15)])?; 26 | }, 27 | } 28 | } 29 | Ok(()) 30 | } 31 | 32 | #[inline] 33 | fn from_hex_digit(digit: u8) -> Option { 34 | match digit { 35 | b'0'..=b'9' => Some(digit - b'0'), 36 | b'A'..=b'F' => Some(digit - b'A' + 10), 37 | b'a'..=b'f' => Some(digit - b'a' + 10), 38 | _ => None, 39 | } 40 | } 41 | 42 | #[inline] 43 | fn to_hex_digit(digit: u8) -> u8 { 44 | match digit { 45 | 0..=9 => b'0' + digit, 46 | 10..=255 => b'A' - 10 + digit, 47 | } 48 | } 49 | 50 | pub fn decode(string: &str) -> Result { 51 | let mut out: Vec = Vec::with_capacity(string.len()); 52 | let mut bytes = string.as_bytes().iter().copied(); 53 | while let Some(b) = bytes.next() { 54 | match b { 55 | b'%' => { 56 | match bytes.next() { 57 | Some(first) => match from_hex_digit(first) { 58 | Some(first_val) => match bytes.next() { 59 | Some(second) => match from_hex_digit(second) { 60 | Some(second_val) => { 61 | out.push((first_val << 4) | second_val); 62 | }, 63 | None => { 64 | out.push(b'%'); 65 | out.push(first); 66 | out.push(second); 67 | }, 68 | }, 69 | None => { 70 | out.push(b'%'); 71 | out.push(first); 72 | }, 73 | }, 74 | None => { 75 | out.push(b'%'); 76 | out.push(first); 77 | }, 78 | }, 79 | None => out.push(b'%'), 80 | }; 81 | }, 82 | other => out.push(other), 83 | } 84 | } 85 | String::from_utf8(out).map_err(|error| FromUrlEncodingError::Utf8CharacterError {error}) 86 | } 87 | 88 | #[derive(Debug)] 89 | pub enum FromUrlEncodingError { 90 | UriCharacterError { character: char, index: usize }, 91 | Utf8CharacterError { error: FromUtf8Error }, 92 | } 93 | 94 | impl Error for FromUrlEncodingError { 95 | fn source(&self) -> Option<&(dyn Error + 'static)> { 96 | match self { 97 | &FromUrlEncodingError::UriCharacterError {character: _, index: _} => None, 98 | &FromUrlEncodingError::Utf8CharacterError {ref error} => Some(error) 99 | } 100 | } 101 | } 102 | 103 | impl Display for FromUrlEncodingError { 104 | fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { 105 | match self { 106 | &FromUrlEncodingError::UriCharacterError {character, index} => 107 | write!(f, "invalid URI char [{}] at [{}]", character, index), 108 | &FromUrlEncodingError::Utf8CharacterError {ref error} => 109 | write!(f, "invalid utf8 char: {}", error) 110 | } 111 | } 112 | } 113 | 114 | #[cfg(test)] 115 | mod tests { 116 | use super::encode; 117 | use super::decode; 118 | use super::from_hex_digit; 119 | 120 | #[test] 121 | fn it_encodes_successfully() { 122 | let expected = "this%20that"; 123 | assert_eq!(expected, encode("this that")); 124 | } 125 | 126 | #[test] 127 | fn it_encodes_successfully_emoji() { 128 | let emoji_string = "👾 Exterminate!"; 129 | let expected = "%F0%9F%91%BE%20Exterminate%21"; 130 | assert_eq!(expected, encode(emoji_string)); 131 | } 132 | 133 | #[test] 134 | fn it_decodes_successfully() { 135 | let expected = String::from("this that"); 136 | let encoded = "this%20that"; 137 | assert_eq!(expected, decode(encoded).unwrap()); 138 | } 139 | 140 | #[test] 141 | fn it_decodes_successfully_emoji() { 142 | let expected = String::from("👾 Exterminate!"); 143 | let encoded = "%F0%9F%91%BE%20Exterminate%21"; 144 | assert_eq!(expected, decode(encoded).unwrap()); 145 | } 146 | 147 | #[test] 148 | fn it_decodes_unsuccessfully_emoji() { 149 | let bad_encoded_string = "👾 Exterminate!"; 150 | 151 | assert_eq!(bad_encoded_string, decode(bad_encoded_string).unwrap()); 152 | } 153 | 154 | 155 | #[test] 156 | fn misc() { 157 | assert_eq!(3, from_hex_digit(b'3').unwrap()); 158 | assert_eq!(10, from_hex_digit(b'a').unwrap()); 159 | assert_eq!(15, from_hex_digit(b'F').unwrap()); 160 | assert_eq!(None, from_hex_digit(b'G')); 161 | assert_eq!(None, from_hex_digit(9)); 162 | 163 | assert_eq!("pureascii", encode("pureascii")); 164 | assert_eq!("pureascii", decode("pureascii").unwrap()); 165 | assert_eq!("", encode("")); 166 | assert_eq!("", decode("").unwrap()); 167 | assert_eq!("%00", encode("\0")); 168 | assert_eq!("\0", decode("\0").unwrap()); 169 | assert!(decode("%F0%0F%91%BE%20Hello%21").is_err()); 170 | assert_eq!("this%2that", decode("this%2that").unwrap()); 171 | assert_eq!("this that", decode("this%20that").unwrap()); 172 | assert_eq!("this that%", decode("this%20that%").unwrap()); 173 | assert_eq!("this that%2", decode("this%20that%2").unwrap()); 174 | } 175 | } 176 | --------------------------------------------------------------------------------