From da05a1d3182e946ecdc6d84a0c5144f7997b18bd Mon Sep 17 00:00:00 2001 From: Kalev Gonvick Date: Sun, 12 Apr 2026 14:28:49 -0700 Subject: [PATCH] Cleaned up some code; Added new builder struct; Added test cases for builder --- benches/encode_benchmarks.rs | 5 +- src/builder.rs | 134 +++++++++++++++++++++++++++++++++++ src/common.rs | 26 ++++--- src/java_script_encoder.rs | 15 ++-- src/lib.rs | 19 +++-- src/uri_encoder.rs | 19 ++--- src/xml_encoder.rs | 33 ++------- 7 files changed, 188 insertions(+), 63 deletions(-) create mode 100644 src/builder.rs diff --git a/benches/encode_benchmarks.rs b/benches/encode_benchmarks.rs index 65a1ca4..97c85f4 100644 --- a/benches/encode_benchmarks.rs +++ b/benches/encode_benchmarks.rs @@ -1,7 +1,6 @@ use criterion::{Criterion, criterion_group, criterion_main}; -use tiny_clean::java_script_encoder::{JavaScriptEncoder, JavaScriptEncoderMode}; -use tiny_clean::uri_encoder::{UriEncoder, UriEncoderMode}; -use tiny_clean::xml_encoder::{XmlEncoder, XmlEncoderMode}; + +use tiny_clean::{EncoderBuilder, UriEncoderMode, UriEncoder, XmlEncoderMode, XmlEncoder, JavaScriptEncoderMode, JavaScriptEncoder}; fn xml_encode_benches(c: &mut Criterion) { let bench_data1 = std::fs::read_to_string("./benches/data/benchmark-data-1.txt").unwrap(); diff --git a/src/builder.rs b/src/builder.rs new file mode 100644 index 0000000..66b9626 --- /dev/null +++ b/src/builder.rs @@ -0,0 +1,134 @@ +use crate::java_script_encoder::{JavaScriptEncoder, JavaScriptEncoderMode}; +use crate::uri_encoder::{UriEncoder, UriEncoderMode}; +use crate::xml_encoder::{XmlEncoder, XmlEncoderMode}; + +pub struct NoBuilder; +pub struct JavaScriptEncoderBuilder { + ascii_only: bool, + mode: JavaScriptEncoderMode, +} +pub struct XmlEncoderBuilder { + mode: XmlEncoderMode, +} + +pub struct UriEncoderBuilder { + mode: UriEncoderMode, +} + +pub struct EncoderBuilder { + state: S, +} + +impl EncoderBuilder { + pub fn new() -> Self { + EncoderBuilder { state: NoBuilder } + } + + pub fn java_script(self) -> EncoderBuilder { + EncoderBuilder { + state: JavaScriptEncoderBuilder { + ascii_only: false, + mode: JavaScriptEncoderMode::Block, + }, + } + } + + pub fn xml(self) -> EncoderBuilder { + EncoderBuilder { + state: XmlEncoderBuilder { + mode: XmlEncoderMode::All, + }, + } + } + + pub fn uri(self) -> EncoderBuilder { + EncoderBuilder { + state: UriEncoderBuilder { + mode: UriEncoderMode::FullUri, + }, + } + } +} + +impl EncoderBuilder { + pub fn with_ascii_only(self, ascii_only: bool) -> Self { + let mut state = self.state; + state.ascii_only = ascii_only; + Self { state } + } + + pub fn with_encoder_mode(self, mode: JavaScriptEncoderMode) -> Self { + let mut state = self.state; + state.mode = mode; + Self { state } + } + + pub fn build(self) -> JavaScriptEncoder { + JavaScriptEncoder::new(self.state.mode, self.state.ascii_only) + } +} + +impl EncoderBuilder { + pub fn with_mode(self, mode: XmlEncoderMode) -> Self { + let mut state = self.state; + state.mode = mode; + Self { state } + } + + pub fn build(self) -> XmlEncoder { + XmlEncoder::new(self.state.mode) + } +} + +impl EncoderBuilder { + pub fn with_mode(self, mode: UriEncoderMode) -> Self { + let mut state = self.state; + state.mode = mode; + Self { state } + } + + pub fn build(self) -> UriEncoder { + UriEncoder::new(self.state.mode) + } +} + +#[cfg(test)] +mod test { + use crate::builder::EncoderBuilder; + use crate::java_script_encoder::JavaScriptEncoderMode; + use crate::java_script_encoder::JavaScriptEncoderMode::Attribute; + use crate::uri_encoder::UriEncoderMode; + use crate::xml_encoder::XmlEncoderMode; + + #[test] + fn test_build_java_script_encoder() { + let encoder = EncoderBuilder::new() + .java_script() + .with_encoder_mode(JavaScriptEncoderMode::Attribute) + .with_ascii_only(true); + + assert_eq!(true, encoder.state.ascii_only); + assert_eq!(Attribute, encoder.state.mode); + + let encoder = encoder.build(); + + assert_eq!(true, encoder.ascii_only); + assert_eq!(true, encoder.hex_encode_quotes); + } + + #[test] + fn test_build_xml_encoder() { + let encoder = EncoderBuilder::new() + .xml() + .with_mode(XmlEncoderMode::Content); + + assert_eq!(XmlEncoderMode::Content, encoder.state.mode); + } + + #[test] + fn test_build_uri_encoder() { + let encoder = EncoderBuilder::new().uri().with_mode(UriEncoderMode::Component); + + assert_eq!(UriEncoderMode::Component, encoder.state.mode); + } +} diff --git a/src/common.rs b/src/common.rs index 283b078..9596fc6 100644 --- a/src/common.rs +++ b/src/common.rs @@ -17,8 +17,8 @@ pub(crate) const U_HEX: [char; 16] = [ /// - Shifts the value `1` to the left by the result of the calculation, creating a bitmask where /// only one specific bit is set. /// -#[inline] -pub(crate) const fn char_mask(c: char) -> u32 { +#[inline(always)] +pub const fn char_mask(c: char) -> u32 { 1 << (c as u32 & 31) } @@ -28,8 +28,8 @@ pub(crate) const fn char_mask(c: char) -> u32 { /// - Converts the character `c` into its Unicode scalar value (`u32`). /// - Performs a bitwise right shift by 5 (`c as u32 >> 5`), which is equivalent to integer division by 32. /// - Casts the resulting value to `usize` for use as an index or bucket identifier in further operations. -#[inline] -pub(crate) const fn char_bucket(c: char) -> usize { +#[inline(always)] +pub const fn char_bucket(c: char) -> usize { (c as u32 >> 5) as usize } @@ -48,8 +48,8 @@ pub(crate) const fn char_bucket(c: char) -> usize { /// value of the input character. /// - This function uses the constants `HEX`, `HEX_SHIFT`, and `HEX_MASK` to efficiently extract and format /// the hexadecimal digits. -#[inline] -pub(crate) fn encode_as_hex_byte(escape_char: char, output: &mut String, character: char) { +#[inline(always)] +pub fn encode_as_hex_byte(escape_char: char, output: &mut String, character: char) { output.push(escape_char); output.push('x'); output.push(HEX[(character as u32 >> HEX_SHIFT) as usize]); @@ -71,8 +71,8 @@ pub(crate) fn encode_as_hex_byte(escape_char: char, output: &mut String, charact /// hexadecimal Unicode code point of the input character. /// - Hexadecimal digits are efficiently calculated and appended to the output using bitwise operations /// and the `HEX` lookup table. -#[inline] -pub(crate) fn encode_as_unicode(escape_char: char, output: &mut String, character: char) { +#[inline(always)] +pub fn encode_as_unicode(escape_char: char, output: &mut String, character: char) { output.push(escape_char); output.push('u'); output.push(HEX[(character as u32 >> (3 * HEX_SHIFT)) as usize & HEX_MASK as usize]); @@ -81,7 +81,15 @@ pub(crate) fn encode_as_unicode(escape_char: char, output: &mut String, characte output.push(HEX[(character as u32 & HEX_MASK) as usize]); } -pub(crate) fn dump_masks_to_ascii(masks: &[u32; 4]) { +#[inline(always)] +pub fn create_mask(chars: [char; N]) -> u32 { + let mut mask = 0; + chars.iter().for_each(|char| mask |= char_mask(*char)); + mask +} + +#[cfg(debug_assertions)] +pub fn dump_masks_to_ascii(masks: &[u32; 4]) { println!("Dumping Mask Values (0-127)"); for char in '\u{0000}'..='\u{007F}' { let bucket = char_bucket(char); diff --git a/src/java_script_encoder.rs b/src/java_script_encoder.rs index ee406b4..05dd6ef 100644 --- a/src/java_script_encoder.rs +++ b/src/java_script_encoder.rs @@ -1,4 +1,5 @@ -use crate::common::{char_bucket, char_mask, encode_as_hex_byte, encode_as_unicode, dump_masks_to_ascii}; +use crate::common::{char_bucket, char_mask, encode_as_hex_byte, encode_as_unicode}; + #[derive(Debug, Clone, Copy, PartialEq)] pub enum JavaScriptEncoderMode { @@ -8,10 +9,11 @@ pub enum JavaScriptEncoderMode { Attribute, } +#[non_exhaustive] pub struct JavaScriptEncoder { - ascii_only: bool, - valid_masks: [u32; 4], - hex_encode_quotes: bool, + pub ascii_only: bool, + pub valid_masks: [u32; 4], + pub hex_encode_quotes: bool, } impl JavaScriptEncoder { @@ -36,9 +38,8 @@ impl JavaScriptEncoder { valid_masks[1] &= !char_mask('&'); } - if cfg!(debug_assertions) { - dump_masks_to_ascii(&valid_masks); - } + #[cfg(debug_assertions)] + crate::common::dump_masks_to_ascii(&valid_masks); let hex_encode_quotes = mode == JavaScriptEncoderMode::Attribute || mode == JavaScriptEncoderMode::Html; JavaScriptEncoder { diff --git a/src/lib.rs b/src/lib.rs index 7d783fb..b5d6986 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,18 @@ -pub mod common; -pub mod uri_encoder; -pub mod java_script_encoder; -pub mod xml_encoder; +mod common; +mod uri_encoder; +mod java_script_encoder; +mod xml_encoder; +mod builder; + +pub use builder::EncoderBuilder; + +pub use xml_encoder::{XmlEncoderMode, XmlEncoder}; +pub use java_script_encoder::{JavaScriptEncoder, JavaScriptEncoderMode}; +pub use uri_encoder::{UriEncoderMode, UriEncoder}; + +#[cfg(debug_assertions)] +pub use common::dump_masks_to_ascii; + diff --git a/src/uri_encoder.rs b/src/uri_encoder.rs index e84074c..47ef046 100644 --- a/src/uri_encoder.rs +++ b/src/uri_encoder.rs @@ -1,4 +1,4 @@ -use crate::common::{HEX_MASK, HEX_SHIFT, U_HEX, char_bucket, char_mask}; +use crate::common::{HEX_MASK, HEX_SHIFT, U_HEX, char_bucket, char_mask, create_mask}; /// 0111_1111_1111 --> highest 2x utf 8 bytes /// 0000_1000_0000 --> most sig. utf8 byte @@ -16,6 +16,7 @@ const UTF8_4_BYTE_FIRST_MSB: u32 = 0b_0000_1111_0000; const UTF8_SHIFT: u32 = 0b_0000_0000_0110; const UTF8_MASK: u32 = 0b_0000_0011_1111; +#[derive(Debug, Clone, Copy, Eq, PartialEq)] pub enum UriEncoderMode { Component, FullUri, @@ -50,20 +51,10 @@ impl UriEncoder { Self { valid_masks } } UriEncoderMode::FullUri => { - let reserved_chars1 = [ + let uri_reserved_bucket1: u32 = create_mask([ '!', '#', '$', '?', '&', '(', ')', '*', '+', ',', ':', ';', '=', '/', '\'', - ]; - let mut uri_reserved_bucket1: u32 = 0; - for reserved in reserved_chars1 { - uri_reserved_bucket1 |= char_mask(reserved); - } - - let reserved_chars2 = ['[', ']', '@']; - let mut uri_reserved_bucket2: u32 = 0; - for reserved in reserved_chars2 { - uri_reserved_bucket2 |= char_mask(reserved); - } - + ]); + let uri_reserved_bucket2: u32 = create_mask(['[', ']', '@']); let valid_masks = [ 0, uri_unreserved_bucket1 | uri_reserved_bucket1, diff --git a/src/xml_encoder.rs b/src/xml_encoder.rs index 493712c..7e49fc1 100644 --- a/src/xml_encoder.rs +++ b/src/xml_encoder.rs @@ -1,5 +1,6 @@ -use crate::common::{char_bucket, char_mask}; +use crate::common::{char_bucket, char_mask, create_mask}; +#[derive(Debug, Clone, Copy, Eq, PartialEq)] pub enum XmlEncoderMode { All, Content, @@ -17,11 +18,7 @@ impl XmlEncoder { let base_mask = char_mask('\r') | char_mask('\t') | char_mask('\n'); match mode { XmlEncoderMode::All => { - let to_be_encoded = ['&', '<', '>', '\'', '"']; - let mut to_be_encoded_mask = 0u32; - for char in to_be_encoded { - to_be_encoded_mask |= char_mask(char); - } + let to_be_encoded_mask = create_mask(['&', '<', '>', '\'', '"']); let valid_masks = [ base_mask, u32::MAX & !to_be_encoded_mask, @@ -31,11 +28,7 @@ impl XmlEncoder { Self {valid_masks} } XmlEncoderMode::Content => { - let to_be_encoded = ['&', '<', '>']; - let mut to_be_encoded_mask = 0u32; - for char in to_be_encoded { - to_be_encoded_mask |= char_mask(char); - } + let to_be_encoded_mask = create_mask(['&', '<', '>']); let valid_masks = [ base_mask, u32::MAX & !to_be_encoded_mask, @@ -45,11 +38,7 @@ impl XmlEncoder { Self {valid_masks} } XmlEncoderMode::Attribute => { - let to_be_encoded = ['&', '<', '\'', '"']; - let mut to_be_encoded_mask = 0u32; - for char in to_be_encoded { - to_be_encoded_mask |= char_mask(char); - } + let to_be_encoded_mask = create_mask(['&', '<', '\'', '"']); let valid_masks = [ base_mask, u32::MAX & !to_be_encoded_mask, @@ -59,11 +48,7 @@ impl XmlEncoder { Self {valid_masks} } XmlEncoderMode::SingleQuotedAttribute => { - let to_be_encoded = ['&', '<', '\'']; - let mut to_be_encoded_mask = 0u32; - for char in to_be_encoded { - to_be_encoded_mask |= char_mask(char); - } + let to_be_encoded_mask = create_mask(['&', '<', '\'']); let valid_masks = [ base_mask, u32::MAX & !to_be_encoded_mask, @@ -73,11 +58,7 @@ impl XmlEncoder { Self {valid_masks} } XmlEncoderMode::DoubleQuotedAttribute => { - let to_be_encoded = ['&', '<', '"']; - let mut to_be_encoded_mask = 0u32; - for char in to_be_encoded { - to_be_encoded_mask |= char_mask(char); - } + let to_be_encoded_mask = create_mask(['&', '<', '"']); let valid_masks = [ base_mask, u32::MAX & !to_be_encoded_mask,