safe_uri 0.1.0-beta.4

Simple and safe URI types.
Documentation
use std::fmt;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct PercentEncoded {
    pub byte: u8,
}

impl PercentEncoded {
    pub const BYTES_LENGTH: usize = 3;

    #[cfg(test)]
    pub const fn from_bytes(bytes: &[u8]) -> Result<Self, InvalidPercentEncoded> {
        Self::from_bytes_at_index(bytes, 0)
    }

    pub const fn from_bytes_at_index(
        bytes: &[u8],
        index: usize,
    ) -> Result<Self, InvalidPercentEncoded> {
        if index + 2 >= bytes.len() {
            return Err(InvalidPercentEncoded { hex_digits: None });
        }
        Self::from_hex_digits([bytes[index + 1], bytes[index + 2]])
    }

    pub const fn from_hex_digits(hex_digits: [u8; 2]) -> Result<Self, InvalidPercentEncoded> {
        let invalid = Err(InvalidPercentEncoded {
            hex_digits: Some(hex_digits),
        });
        let hex_byte_0 = match hex_digit_to_byte(hex_digits[0]) {
            Some(b) => b,
            None => return invalid,
        };
        let hex_byte_1 = match hex_digit_to_byte(hex_digits[1]) {
            Some(b) => b,
            None => return invalid,
        };
        Ok(Self {
            byte: hex_byte_0 * 16 + hex_byte_1,
        })
    }
}

const fn hex_digit_to_byte(hex_digit: u8) -> Option<u8> {
    Some(match hex_digit {
        b'0' => 0,
        b'1' => 1,
        b'2' => 2,
        b'3' => 3,
        b'4' => 4,
        b'5' => 5,
        b'6' => 6,
        b'7' => 7,
        b'8' => 8,
        b'9' => 9,
        b'A' => 10,
        b'B' => 11,
        b'C' => 12,
        b'D' => 13,
        b'E' => 14,
        b'F' => 15,
        _ => return None,
    })
}

#[derive(Debug, Clone)]
pub(crate) struct InvalidPercentEncoded {
    hex_digits: Option<[u8; 2]>,
}

impl fmt::Display for InvalidPercentEncoded {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "percent encoding ")?;
        match self.hex_digits {
            Some([a, b]) => write!(f, "%{}{}", char::from(a), char::from(b))?,
            None => write!(f, "%")?,
        };
        write!(f, " is not valid")
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use assert_matches::assert_matches;

    #[test]
    fn percent_1_g() {
        assert_matches!(
            PercentEncoded::from_bytes(b"%1G"),
            Err(InvalidPercentEncoded {
                hex_digits: Some([b'1', b'G']),
            })
        );
    }

    #[test]
    fn space_percent_1_g() {
        assert_matches!(
            PercentEncoded::from_bytes_at_index(b" %1G", 1),
            Err(InvalidPercentEncoded {
                hex_digits: Some([b'1', b'G']),
            })
        );
    }

    #[test]
    fn percent_f_f() {
        let actual = PercentEncoded::from_bytes(b"%FF").unwrap().byte;
        assert_eq!(actual, u8::MAX);
    }

    #[test]
    fn percent_1_2() {
        let actual = PercentEncoded::from_bytes(b"%12").unwrap().byte;
        assert_eq!(actual, 18);
    }

    #[test]
    fn space_percent_f_f() {
        let actual = PercentEncoded::from_bytes_at_index(b" %FF", 1)
            .unwrap()
            .byte;
        assert_eq!(actual, u8::MAX);
    }

    #[test]
    fn percent_f_f_lower_case() {
        assert_matches!(
            PercentEncoded::from_bytes(b"%ff"),
            Err(InvalidPercentEncoded {
                hex_digits: Some([b'f', b'f']),
            })
        );
    }

    #[test]
    fn negative_sign_is_error() {
        assert_matches!(
            PercentEncoded::from_bytes(b"%-A"),
            Err(InvalidPercentEncoded {
                hex_digits: Some([b'-', b'A']),
            })
        );
    }

    #[test]
    fn positive_sign_is_error() {
        assert_matches!(
            PercentEncoded::from_bytes(b"%+B"),
            Err(InvalidPercentEncoded {
                hex_digits: Some([b'+', b'B']),
            })
        );
    }

    #[test]
    fn qc_byte_equals_from_str_radix() {
        quickcheck::quickcheck(test_byte_equals_from_str_radix as fn(u8, u8))
    }

    fn test_byte_equals_from_str_radix(byte_a: u8, byte_b: u8) {
        if matches!(char::from(byte_a), '+' | '-')
            || byte_a.is_ascii_lowercase()
            || byte_b.is_ascii_lowercase()
        {
            return;
        }
        let bytes = [byte_a, byte_b];
        let percent_encoded_bytes = Vec::from([b'%', byte_a, byte_b]);
        let expected = std::str::from_utf8(&bytes)
            .ok()
            .and_then(|s| u8::from_str_radix(s, 16).ok());
        let actual = PercentEncoded::from_bytes(&percent_encoded_bytes)
            .ok()
            .map(|x| x.byte);
        assert_eq!(actual, expected);
    }
}