214 lines
5.7 KiB
Rust
214 lines
5.7 KiB
Rust
// Copyright 2013-2016 The rust-url developers.
|
||
//
|
||
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
|
||
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
|
||
// option. This file may not be copied, modified, or distributed
|
||
// except according to those terms.
|
||
|
||
use core::{mem, ops};
|
||
|
||
/// Represents a set of characters or bytes in the ASCII range.
|
||
///
|
||
/// This is used in [`percent_encode`] and [`utf8_percent_encode`].
|
||
/// This is similar to [percent-encode sets](https://url.spec.whatwg.org/#percent-encoded-bytes).
|
||
///
|
||
/// Use the `add` method of an existing set to define a new set. For example:
|
||
///
|
||
/// [`percent_encode`]: crate::percent_encode
|
||
/// [`utf8_percent_encode`]: crate::utf8_percent_encode
|
||
///
|
||
/// ```
|
||
/// use percent_encoding::{AsciiSet, CONTROLS};
|
||
///
|
||
/// /// https://url.spec.whatwg.org/#fragment-percent-encode-set
|
||
/// const FRAGMENT: &AsciiSet = &CONTROLS.add(b' ').add(b'"').add(b'<').add(b'>').add(b'`');
|
||
/// ```
|
||
#[derive(Debug, PartialEq, Eq)]
|
||
pub struct AsciiSet {
|
||
mask: [Chunk; ASCII_RANGE_LEN / BITS_PER_CHUNK],
|
||
}
|
||
|
||
type Chunk = u32;
|
||
|
||
const ASCII_RANGE_LEN: usize = 0x80;
|
||
|
||
const BITS_PER_CHUNK: usize = 8 * mem::size_of::<Chunk>();
|
||
|
||
impl AsciiSet {
|
||
/// An empty set.
|
||
pub const EMPTY: Self = Self {
|
||
mask: [0; ASCII_RANGE_LEN / BITS_PER_CHUNK],
|
||
};
|
||
|
||
/// Called with UTF-8 bytes rather than code points.
|
||
/// Not used for non-ASCII bytes.
|
||
pub(crate) const fn contains(&self, byte: u8) -> bool {
|
||
let chunk = self.mask[byte as usize / BITS_PER_CHUNK];
|
||
let mask = 1 << (byte as usize % BITS_PER_CHUNK);
|
||
(chunk & mask) != 0
|
||
}
|
||
|
||
pub(crate) fn should_percent_encode(&self, byte: u8) -> bool {
|
||
!byte.is_ascii() || self.contains(byte)
|
||
}
|
||
|
||
pub const fn add(&self, byte: u8) -> Self {
|
||
let mut mask = self.mask;
|
||
mask[byte as usize / BITS_PER_CHUNK] |= 1 << (byte as usize % BITS_PER_CHUNK);
|
||
Self { mask }
|
||
}
|
||
|
||
pub const fn remove(&self, byte: u8) -> Self {
|
||
let mut mask = self.mask;
|
||
mask[byte as usize / BITS_PER_CHUNK] &= !(1 << (byte as usize % BITS_PER_CHUNK));
|
||
Self { mask }
|
||
}
|
||
|
||
/// Return the union of two sets.
|
||
pub const fn union(&self, other: Self) -> Self {
|
||
let mask = [
|
||
self.mask[0] | other.mask[0],
|
||
self.mask[1] | other.mask[1],
|
||
self.mask[2] | other.mask[2],
|
||
self.mask[3] | other.mask[3],
|
||
];
|
||
Self { mask }
|
||
}
|
||
|
||
/// Return the negation of the set.
|
||
pub const fn complement(&self) -> Self {
|
||
let mask = [!self.mask[0], !self.mask[1], !self.mask[2], !self.mask[3]];
|
||
Self { mask }
|
||
}
|
||
}
|
||
|
||
impl ops::Add for AsciiSet {
|
||
type Output = Self;
|
||
|
||
fn add(self, other: Self) -> Self {
|
||
self.union(other)
|
||
}
|
||
}
|
||
|
||
impl ops::Not for AsciiSet {
|
||
type Output = Self;
|
||
|
||
fn not(self) -> Self {
|
||
self.complement()
|
||
}
|
||
}
|
||
|
||
/// The set of 0x00 to 0x1F (C0 controls), and 0x7F (DEL).
|
||
///
|
||
/// Note that this includes the newline and tab characters, but not the space 0x20.
|
||
///
|
||
/// <https://url.spec.whatwg.org/#c0-control-percent-encode-set>
|
||
pub const CONTROLS: &AsciiSet = &AsciiSet {
|
||
mask: [
|
||
!0_u32, // C0: 0x00 to 0x1F (32 bits set)
|
||
0,
|
||
0,
|
||
1 << (0x7F_u32 % 32), // DEL: 0x7F (one bit set)
|
||
],
|
||
};
|
||
|
||
macro_rules! static_assert {
|
||
($( $bool: expr, )+) => {
|
||
fn _static_assert() {
|
||
$(
|
||
let _ = mem::transmute::<[u8; $bool as usize], u8>;
|
||
)+
|
||
}
|
||
}
|
||
}
|
||
|
||
static_assert! {
|
||
CONTROLS.contains(0x00),
|
||
CONTROLS.contains(0x1F),
|
||
!CONTROLS.contains(0x20),
|
||
!CONTROLS.contains(0x7E),
|
||
CONTROLS.contains(0x7F),
|
||
}
|
||
|
||
/// Everything that is not an ASCII letter or digit.
|
||
///
|
||
/// This is probably more eager than necessary in any context.
|
||
pub const NON_ALPHANUMERIC: &AsciiSet = &CONTROLS
|
||
.add(b' ')
|
||
.add(b'!')
|
||
.add(b'"')
|
||
.add(b'#')
|
||
.add(b'$')
|
||
.add(b'%')
|
||
.add(b'&')
|
||
.add(b'\'')
|
||
.add(b'(')
|
||
.add(b')')
|
||
.add(b'*')
|
||
.add(b'+')
|
||
.add(b',')
|
||
.add(b'-')
|
||
.add(b'.')
|
||
.add(b'/')
|
||
.add(b':')
|
||
.add(b';')
|
||
.add(b'<')
|
||
.add(b'=')
|
||
.add(b'>')
|
||
.add(b'?')
|
||
.add(b'@')
|
||
.add(b'[')
|
||
.add(b'\\')
|
||
.add(b']')
|
||
.add(b'^')
|
||
.add(b'_')
|
||
.add(b'`')
|
||
.add(b'{')
|
||
.add(b'|')
|
||
.add(b'}')
|
||
.add(b'~');
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
#[test]
|
||
fn add_op() {
|
||
let left = AsciiSet::EMPTY.add(b'A');
|
||
let right = AsciiSet::EMPTY.add(b'B');
|
||
let expected = AsciiSet::EMPTY.add(b'A').add(b'B');
|
||
assert_eq!(left + right, expected);
|
||
}
|
||
|
||
#[test]
|
||
fn not_op() {
|
||
let set = AsciiSet::EMPTY.add(b'A').add(b'B');
|
||
let not_set = !set;
|
||
assert!(!not_set.contains(b'A'));
|
||
assert!(not_set.contains(b'C'));
|
||
}
|
||
|
||
/// This test ensures that we can get the union of two sets as a constant value, which is
|
||
/// useful for defining sets in a modular way.
|
||
#[test]
|
||
fn union() {
|
||
const A: AsciiSet = AsciiSet::EMPTY.add(b'A');
|
||
const B: AsciiSet = AsciiSet::EMPTY.add(b'B');
|
||
const UNION: AsciiSet = A.union(B);
|
||
const EXPECTED: AsciiSet = AsciiSet::EMPTY.add(b'A').add(b'B');
|
||
assert_eq!(UNION, EXPECTED);
|
||
}
|
||
|
||
/// This test ensures that we can get the complement of a set as a constant value, which is
|
||
/// useful for defining sets in a modular way.
|
||
#[test]
|
||
fn complement() {
|
||
const BOTH: AsciiSet = AsciiSet::EMPTY.add(b'A').add(b'B');
|
||
const COMPLEMENT: AsciiSet = BOTH.complement();
|
||
assert!(!COMPLEMENT.contains(b'A'));
|
||
assert!(!COMPLEMENT.contains(b'B'));
|
||
assert!(COMPLEMENT.contains(b'C'));
|
||
}
|
||
}
|