product-os-urlencoding 0.0.1

Product OS : URL Encoding is a derivative of the urlencoding crate restructured for both std and no_std environments.
Documentation
#![no_std]
extern crate no_std_compat as std;
extern crate alloc;

use std::prelude::v1::*;

use std::string::String;
use std::string::FromUtf8Error;
use std::fmt::{self, Display};

pub fn encode(data: &str) -> String {
    let escaped = encode_into(data);
    let string = String::from_utf8_lossy(escaped.as_slice());
    string.to_string()
}

#[inline]
fn encode_into(data: &str) -> Vec<u8> {
    let mut escaped = vec![];

    for byte in data.as_bytes().iter() {
        match byte.to_owned() {
            b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z' |  b'-' | b'.' | b'_' | b'~' => {
                escaped.push(byte.to_owned());
            },
            other => {
                escaped.push(b'%');
                escaped.push(to_hex_digit(other.to_owned() >> 4));
                escaped.push(to_hex_digit(other & 15));
            },
        }
    }

    escaped
}

#[inline]
fn from_hex_digit(digit: u8) -> Option<u8> {
    match digit {
        b'0'..=b'9' => Some(digit - b'0'),
        b'A'..=b'F' => Some(digit - b'A' + 10),
        b'a'..=b'f' => Some(digit - b'a' + 10),
        _ => None,
    }
}

#[inline]
fn to_hex_digit(digit: u8) -> u8 {
    match digit {
        0..=9 => b'0' + digit,
        10..=255 => b'A' - 10 + digit
    }
}

pub fn decode(string: &str) -> Result<String, FromUrlEncodingError> {
    let mut out: Vec<u8> = vec![];
    let mut bytes = string.as_bytes().iter().copied();
    while let Some(b) = bytes.next() {
        match b {
            b'%' => {
                match bytes.next() {
                    Some(first) => match from_hex_digit(first.to_owned()) {
                        Some(first_val) => match bytes.next() {
                            Some(second) => match from_hex_digit(second.to_owned()) {
                                Some(second_val) => {
                                    out.push((first_val << 4) | second_val);
                                },
                                None => {
                                    out.push(b'%');
                                    out.push(first);
                                    out.push(second);
                                },
                            },
                            None => {
                                out.push(b'%');
                                out.push(first);
                            },
                        },
                        None => {
                            out.push(b'%');
                            out.push(first);
                        },
                    },
                    None => out.push(b'%'),
                };
            },
            other => out.push(other),
        }
    }
    String::from_utf8(out).map_err(|error| FromUrlEncodingError::Utf8CharacterError {error})
}

#[derive(Debug)]
pub enum FromUrlEncodingError {
    UriCharacterError { character: char, index: usize },
    Utf8CharacterError { error: FromUtf8Error },
}


impl Display for FromUrlEncodingError {
    fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
        match self {
            &FromUrlEncodingError::UriCharacterError {character, index} =>
                write!(f, "invalid URI char [{}] at [{}]", character, index),
            &FromUrlEncodingError::Utf8CharacterError {ref error} =>
                write!(f, "invalid utf8 char: {}", error)
        }
    }
}

#[cfg(test)]
mod tests {
    use alloc::string::String;
    use super::encode;
    use super::decode;
    use super::from_hex_digit;

    #[test]
    fn it_encodes_successfully() {
        let expected = "this%20that";
        assert_eq!(expected, encode("this that"));
    }

    #[test]
    fn it_encodes_successfully_emoji() {
        let emoji_string = "👾 Exterminate!";
        let expected = "%F0%9F%91%BE%20Exterminate%21";
        assert_eq!(expected, encode(emoji_string));
    }

    #[test]
    fn it_decodes_successfully() {
        let expected = String::from("this that");
        let encoded = "this%20that";
        assert_eq!(expected, decode(encoded).unwrap());
    }

    #[test]
    fn it_decodes_successfully_emoji() {
        let expected = String::from("👾 Exterminate!");
        let encoded = "%F0%9F%91%BE%20Exterminate%21";
        assert_eq!(expected, decode(encoded).unwrap());
    }

    #[test]
    fn it_decodes_unsuccessfully_emoji() {
        let bad_encoded_string = "👾 Exterminate!";

        assert_eq!(bad_encoded_string, decode(bad_encoded_string).unwrap());
    }


    #[test]
    fn misc() {
        assert_eq!(3, from_hex_digit(b'3').unwrap());
        assert_eq!(10, from_hex_digit(b'a').unwrap());
        assert_eq!(15, from_hex_digit(b'F').unwrap());
        assert_eq!(None, from_hex_digit(b'G'));
        assert_eq!(None, from_hex_digit(9));

        assert_eq!("pureascii", encode("pureascii"));
        assert_eq!("pureascii", decode("pureascii").unwrap());
        assert_eq!("", encode(""));
        assert_eq!("", decode("").unwrap());
        assert_eq!("%00", encode("\0"));
        assert_eq!("\0", decode("\0").unwrap());
        assert!(decode("%F0%0F%91%BE%20Hello%21").is_err());
        assert_eq!("this%2that", decode("this%2that").unwrap());
        assert_eq!("this that", decode("this%20that").unwrap());
        assert_eq!("this that%", decode("this%20that%").unwrap());
        assert_eq!("this that%2", decode("this%20that%2").unwrap());
    }
}