Skip to main content

wafrift_encoding/encoding/
strategy.rs

1//! Strategy enum and main encode() dispatcher.
2
3use super::invisible::{
4    circled_letter_encode, ligature_encode, parenthesized_letter_encode, soft_hyphen_inject,
5    tag_char_encode, variation_selector_pad, variation_selector_supplementary_pad,
6    word_joiner_wrap,
7};
8use super::keyword::{
9    between_obfuscate, case_alternate, mysql_versioned_comment, percentage_prefix,
10    random_case_alternate, space_to_comment, space_to_dash, space_to_hash, space_to_plus,
11    space_to_random_blank, sql_comment_insert, unmagic_quotes, whitespace_insert,
12};
13use super::structural::{
14    base64_encode, base64_url_encode, chunked_split, deflate_encode, gzip_encode, hex_encode,
15    null_byte_inject, overlong_utf8, overlong_utf8_more, parameter_pollute, utf7_encode,
16};
17use super::unicode::{
18    fullwidth_encode, homoglyph_encode, html_entity_decimal_encode, html_entity_encode,
19    iis_unicode_encode, json_string_encode, unicode_encode,
20};
21use super::url::{double_url_encode, triple_url_encode, url_encode, url_encode_lower};
22use crate::error::EncodeError;
23
24/// Maximum input payload size to prevent OOM on adversarial input.
25pub const MAX_PAYLOAD_SIZE: usize = 8 * 1024 * 1024;
26
27/// Default chunk size for `Strategy::ChunkedSplit`.
28///
29/// 1 KiB chunks are large enough to avoid excessive chunk-count overhead
30/// on typical SQLi payloads (< 1 KB) while small enough that a WAF
31/// scanning only the first chunk misses the rest. Callers needing a
32/// different split granularity can call `structural::chunked_split` directly.
33pub const CHUNKED_SPLIT_DEFAULT_CHUNK_SIZE: usize = 1024;
34
35/// MySQL version number used in `/*!VERSIONKEYWORD*/` versioned comments.
36///
37/// `50000` = MySQL 5.0.0, the baseline for the `/*!...*/` conditional-
38/// execution syntax. Any MySQL 5.0+ instance will execute the wrapped
39/// keyword; WAFs that don't implement the MySQL comment parser will skip it.
40/// Virtually every production MySQL installation targeted by Cloudflare
41/// CumulusFire runs >= 5.0.0.
42pub const MYSQL_VERSIONED_COMMENT_VERSION: u32 = 50_000;
43
44/// Available encoding strategies.
45///
46/// # Context hints
47/// Many strategies are only semantically correct in specific parser contexts.
48/// Use [`Strategy::contexts`] to query the applicable contexts for a strategy.
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
50#[non_exhaustive]
51pub enum Strategy {
52    /// Standard URL encoding (%XX) — preserves unreserved chars per RFC 3986.
53    /// Safe for: query strings, paths, form data.
54    UrlEncode,
55    /// Lowercase hex URL encoding (%xx) — same semantics as `UrlEncode`.
56    /// Safe for: query strings, paths, form data.
57    UrlEncodeLower,
58    /// Double URL encoding (%25XX) — bypasses WAFs that decode once.
59    /// Safe for: query strings, paths, form data.
60    DoubleUrlEncode,
61    /// Triple URL encoding (%2525XX) — bypasses WAFs that decode twice.
62    /// Safe for: query strings, paths, form data.
63    TripleUrlEncode,
64    /// Unicode escape (\uXXXX) — ONLY safe when target parses JSON/JavaScript.
65    /// Unsafe for: raw HTTP parameters, headers, most server frameworks.
66    UnicodeEncode,
67    /// IIS/ASP percent Unicode (%uXXXX) — ONLY safe on IIS/ASP classic parsers.
68    /// Unsafe for: modern servers (nginx, Apache, Node.js, etc.).
69    IisUnicodeEncode,
70    /// JSON string encoding with Unicode escapes — ONLY safe in JSON contexts.
71    /// Unsafe for: raw HTTP parameters.
72    JsonEncode,
73    /// HTML entity encoding (&#xXX;) — ONLY safe in HTML contexts.
74    /// Unsafe for: raw HTTP parameters, JSON bodies.
75    HtmlEntityEncode,
76    /// HTML decimal entity encoding (&#60;) — ONLY safe in HTML contexts.
77    /// Unsafe for: raw HTTP parameters, JSON bodies.
78    HtmlEntityDecimalEncode,
79    /// Alternating case (`SeLeCt`) — bypasses case-sensitive keyword filters.
80    /// Safe for: any text context where case is preserved.
81    CaseAlternation,
82    /// Random alternating case — non-deterministic variant of `CaseAlternation`.
83    /// Safe for: any text context where case is preserved.
84    RandomCase,
85    /// Tab insertion BETWEEN tokens — preserves keyword integrity.
86    /// Safe for: SQL contexts where whitespace separates tokens.
87    WhitespaceInsertion,
88    /// SQL comment insertion BETWEEN tokens — preserves keyword integrity.
89    /// Safe for: SQL contexts where comments are treated as whitespace.
90    SqlCommentInsertion,
91    /// `MySQL` versioned comment (`/*!50000SELECT*/`) — executed by `MySQL`, ignored by WAFs.
92    /// Safe for: `MySQL` backends.
93    MysqlVersionedComment,
94    /// Null byte injection (%00) — ONLY semantically correct for C-style string parsers.
95    /// Context: php, some CGI implementations.
96    NullByte,
97    /// Overlong UTF-8 encoding (2-byte) — ONLY works against legacy WAFs that normalize.
98    /// Context: iis-6, very old frontends.
99    OverlongUtf8,
100    /// Extended overlong UTF-8 encoding (3-byte) — broader coverage than `OverlongUtf8`.
101    /// Context: iis-6, very old frontends.
102    OverlongUtf8More,
103    /// Chunked transfer-encoding split — ONLY valid with `Transfer-Encoding: chunked`.
104    /// Context: http-request-body.
105    ChunkedSplit,
106    /// HTTP parameter pollution — duplicate parameter with benign first value.
107    /// Safe for: query strings, form data.
108    ParameterPollution,
109    /// Base64 encoding (standard alphabet).
110    /// Safe for: headers, bodies, query strings (may need URL encoding after).
111    Base64Encode,
112    /// Base64 URL-safe encoding (-_ no padding).
113    /// Safe for: URL contexts where +/ would be mangled.
114    Base64UrlEncode,
115    /// Hex encoding.
116    /// Safe for: any byte context.
117    HexEncode,
118    /// UTF-7 encoding per RFC 2152.
119    /// Context: legacy IIS/.NET parsers that decode UTF-7.
120    Utf7Encode,
121    /// Gzip compression — ONLY valid with `Content-Encoding: gzip`.
122    /// Context: http-request-body.
123    GzipEncode,
124    /// Deflate compression — ONLY valid with `Content-Encoding: deflate`.
125    /// Context: http-request-body.
126    DeflateEncode,
127    /// Replace spaces with SQL comments (`/**/`).
128    /// Safe for: SQL contexts.
129    SpaceToComment,
130    /// Replace spaces with dash comments (`--`).
131    /// Safe for: SQL contexts.
132    SpaceToDash,
133    /// Replace spaces with hash comments (`#`).
134    /// Safe for: `MySQL` contexts.
135    SpaceToHash,
136    /// Replace spaces with plus signs (`+`).
137    /// Safe for: URL-encoded form data.
138    SpaceToPlus,
139    /// Replace spaces with random blank characters.
140    /// Safe for: SQL contexts.
141    SpaceToRandomBlank,
142    /// Prefix each character with `%` — lightweight bypass.
143    /// Safe for: contexts that strip `%` before parsing.
144    PercentagePrefix,
145    /// Between obfuscation (`=` → `BETWEEN # AND #`).
146    /// Safe for: SQL contexts.
147    BetweenObfuscation,
148    /// Unmagic quotes (`%bf%27`) — multi-byte charset quote escape.
149    /// Context: PHP with GBK/Big5/Shift-JIS connections.
150    UnmagicQuotes,
151    /// Fullwidth Unicode (`SELECTuntouched`) — bypasses ASCII keyword regex.
152    /// Context: backends that perform NFKC normalization (Java, .NET, Python 3, `PostgreSQL`).
153    FullwidthEncode,
154    /// Homoglyph substitution — visually identical Unicode chars for `'`, `"`, `<`, `>`, `=`.
155    /// Context: byte-level WAFs with Unicode-tolerant backends.
156    HomoglyphEncode,
157    /// Plan 9 tag-character encoding — every ASCII byte becomes
158    /// `U+E0000 + byte`. Renders invisible; LLM-WAF tokenizers
159    /// frequently still decode them, defeating keyword filters.
160    /// Context: any (codepoint-level transforms).
161    TagCharEncode,
162    /// Append U+FE0F VARIATION SELECTOR-16 after every codepoint.
163    /// Some normalizers strip it; many WAFs don't.
164    /// Context: any.
165    VariationSelectorPad,
166    /// Same as `VariationSelectorPad` but rotates through the
167    /// supplementary range U+E0100..=U+E01EF (per-position selector).
168    /// Defeats filters that strip the basic VS range only.
169    /// Context: any.
170    VariationSelectorSupplementaryPad,
171    /// Replace `ff`/`fi`/`fl`/`ffi`/`ffl`/`st`/`ſt` with their
172    /// precomposed stylistic ligature codepoints (U+FB00..=U+FB06).
173    /// NFKC decomposes back; pre-NFKC WAFs see opaque codepoints.
174    /// Context: nfkc (origins that NFKC-fold).
175    LigatureEncode,
176    /// Replace ASCII letters with U+24B6..=U+24E9 circled forms.
177    /// NFKC-equivalent to ASCII letters.
178    /// Context: nfkc.
179    CircledLetterEncode,
180    /// Replace ASCII letters with U+1F110..=U+1F12B (upper) /
181    /// U+249C..=U+24B5 (lower) parenthesized forms.
182    /// NFKC-equivalent to ASCII letters. Rotation partner for
183    /// `FullwidthEncode` / `CircledLetterEncode`.
184    /// Context: nfkc.
185    ParenthesizedLetterEncode,
186    /// Inject U+00AD SOFT HYPHEN between every pair of codepoints.
187    /// Visually invisible; some backends strip during normalization.
188    /// Context: any.
189    SoftHyphenInject,
190    /// Wrap each codepoint in U+2060 WORD JOINER.
191    /// Zero-width, NFC-stable, NFKC strips it.
192    /// Context: any.
193    WordJoinerWrap,
194}
195
196impl Strategy {
197    /// Returns the string identifier for this encoding strategy.
198    #[must_use]
199    pub const fn as_str(&self) -> &'static str {
200        match self {
201            Self::UrlEncode => "UrlEncode",
202            Self::UrlEncodeLower => "UrlEncodeLower",
203            Self::DoubleUrlEncode => "DoubleUrlEncode",
204            Self::TripleUrlEncode => "TripleUrlEncode",
205            Self::UnicodeEncode => "UnicodeEncode",
206            Self::IisUnicodeEncode => "IisUnicodeEncode",
207            Self::JsonEncode => "JsonEncode",
208            Self::HtmlEntityEncode => "HtmlEntityEncode",
209            Self::HtmlEntityDecimalEncode => "HtmlEntityDecimalEncode",
210            Self::CaseAlternation => "CaseAlternation",
211            Self::RandomCase => "RandomCase",
212            Self::WhitespaceInsertion => "WhitespaceInsertion",
213            Self::SqlCommentInsertion => "SqlCommentInsertion",
214            Self::MysqlVersionedComment => "MysqlVersionedComment",
215            Self::NullByte => "NullByte",
216            Self::OverlongUtf8 => "OverlongUtf8",
217            Self::OverlongUtf8More => "OverlongUtf8More",
218            Self::ChunkedSplit => "ChunkedSplit",
219            Self::ParameterPollution => "ParameterPollution",
220            Self::Base64Encode => "Base64Encode",
221            Self::Base64UrlEncode => "Base64UrlEncode",
222            Self::HexEncode => "HexEncode",
223            Self::Utf7Encode => "Utf7Encode",
224            Self::GzipEncode => "GzipEncode",
225            Self::DeflateEncode => "DeflateEncode",
226            Self::SpaceToComment => "SpaceToComment",
227            Self::SpaceToDash => "SpaceToDash",
228            Self::SpaceToHash => "SpaceToHash",
229            Self::SpaceToPlus => "SpaceToPlus",
230            Self::SpaceToRandomBlank => "SpaceToRandomBlank",
231            Self::PercentagePrefix => "PercentagePrefix",
232            Self::BetweenObfuscation => "BetweenObfuscation",
233            Self::UnmagicQuotes => "UnmagicQuotes",
234            Self::FullwidthEncode => "FullwidthEncode",
235            Self::HomoglyphEncode => "HomoglyphEncode",
236            Self::TagCharEncode => "TagCharEncode",
237            Self::VariationSelectorPad => "VariationSelectorPad",
238            Self::VariationSelectorSupplementaryPad => "VariationSelectorSupplementaryPad",
239            Self::LigatureEncode => "LigatureEncode",
240            Self::CircledLetterEncode => "CircledLetterEncode",
241            Self::ParenthesizedLetterEncode => "ParenthesizedLetterEncode",
242            Self::SoftHyphenInject => "SoftHyphenInject",
243            Self::WordJoinerWrap => "WordJoinerWrap",
244        }
245    }
246
247    /// Returns the parser contexts where this strategy is semantically safe.
248    ///
249    /// An empty slice means the strategy is generally applicable.
250    /// Callers should gate strategy application by matching these contexts
251    /// against the target type (e.g., `json`, `html`, `sql`, `php`, `iis-6`).
252    #[must_use]
253    pub const fn contexts(&self) -> &'static [&'static str] {
254        match self {
255            Self::UrlEncode
256            | Self::UrlEncodeLower
257            | Self::DoubleUrlEncode
258            | Self::TripleUrlEncode
259            | Self::ParameterPollution => &[],
260            Self::UnicodeEncode => &["json", "javascript"],
261            Self::IisUnicodeEncode => &["iis", "asp"],
262            Self::JsonEncode => &["json"],
263            Self::HtmlEntityEncode | Self::HtmlEntityDecimalEncode => &["html"],
264            Self::CaseAlternation | Self::RandomCase | Self::WhitespaceInsertion => &[],
265            Self::SqlCommentInsertion
266            | Self::MysqlVersionedComment
267            | Self::SpaceToComment
268            | Self::SpaceToDash
269            | Self::SpaceToRandomBlank
270            | Self::BetweenObfuscation => &["sql"],
271            Self::SpaceToHash => &["sql", "mysql"],
272            Self::SpaceToPlus => &["url-encoded"],
273            Self::NullByte => &["php", "cgi"],
274            Self::OverlongUtf8 | Self::OverlongUtf8More => &["iis-6"],
275            Self::ChunkedSplit => &["http-request-body"],
276            Self::Base64Encode | Self::Base64UrlEncode | Self::HexEncode => &[],
277            Self::Utf7Encode => &["iis", "legacy-dotnet"],
278            Self::GzipEncode | Self::DeflateEncode => &["http-request-body"],
279            Self::PercentagePrefix => &[],
280            Self::UnmagicQuotes => &["php", "gbk", "big5", "shift-jis"],
281            Self::FullwidthEncode => &["nfkc", "java", "dotnet", "python3", "postgresql"],
282            Self::HomoglyphEncode => &[],
283            Self::TagCharEncode
284            | Self::VariationSelectorPad
285            | Self::VariationSelectorSupplementaryPad
286            | Self::SoftHyphenInject
287            | Self::WordJoinerWrap => &[],
288            Self::LigatureEncode | Self::CircledLetterEncode | Self::ParenthesizedLetterEncode => {
289                &["nfkc"]
290            }
291        }
292    }
293}
294
295fn check_size(payload: &[u8]) -> Result<(), EncodeError> {
296    if payload.len() > MAX_PAYLOAD_SIZE {
297        Err(EncodeError::PayloadTooLarge {
298            max: MAX_PAYLOAD_SIZE,
299            actual: payload.len(),
300        })
301    } else {
302        Ok(())
303    }
304}
305
306/// Encode a payload using the selected strategy.
307///
308/// # Errors
309/// Returns `EncodeError::PayloadTooLarge` if the input exceeds [`MAX_PAYLOAD_SIZE`].
310/// Returns `EncodeError::InvalidUtf8` for text-oriented strategies when the input
311/// contains invalid UTF-8.
312///
313/// # UTF-8 safety
314/// Text-oriented strategies validate UTF-8 via `std::str::from_utf8` and return
315/// `InvalidUtf8` on failure. No unsafe UTF-8 conversions (`from_utf8_unchecked`,
316/// lossy casts, etc.) are used in the encoding pipeline.
317pub fn encode(payload: impl AsRef<[u8]>, strategy: Strategy) -> Result<String, EncodeError> {
318    let payload = payload.as_ref();
319    check_size(payload)?;
320
321    match strategy {
322        Strategy::UrlEncode => Ok(url_encode(payload)),
323        Strategy::UrlEncodeLower => Ok(url_encode_lower(payload)),
324        Strategy::DoubleUrlEncode => Ok(double_url_encode(payload)),
325        Strategy::TripleUrlEncode => Ok(triple_url_encode(payload)),
326        Strategy::UnicodeEncode => {
327            let text = std::str::from_utf8(payload).map_err(|_| EncodeError::InvalidUtf8)?;
328            Ok(unicode_encode(text))
329        }
330        Strategy::IisUnicodeEncode => {
331            let text = std::str::from_utf8(payload).map_err(|_| EncodeError::InvalidUtf8)?;
332            Ok(iis_unicode_encode(text))
333        }
334        Strategy::JsonEncode => {
335            let text = std::str::from_utf8(payload).map_err(|_| EncodeError::InvalidUtf8)?;
336            Ok(json_string_encode(text))
337        }
338        Strategy::HtmlEntityEncode => {
339            let text = std::str::from_utf8(payload).map_err(|_| EncodeError::InvalidUtf8)?;
340            Ok(html_entity_encode(text))
341        }
342        Strategy::HtmlEntityDecimalEncode => {
343            let text = std::str::from_utf8(payload).map_err(|_| EncodeError::InvalidUtf8)?;
344            Ok(html_entity_decimal_encode(text))
345        }
346        Strategy::CaseAlternation => {
347            let text = std::str::from_utf8(payload).map_err(|_| EncodeError::InvalidUtf8)?;
348            Ok(case_alternate(text))
349        }
350        Strategy::RandomCase => {
351            let text = std::str::from_utf8(payload).map_err(|_| EncodeError::InvalidUtf8)?;
352            Ok(random_case_alternate(text))
353        }
354        Strategy::WhitespaceInsertion => {
355            let text = std::str::from_utf8(payload).map_err(|_| EncodeError::InvalidUtf8)?;
356            Ok(whitespace_insert(text))
357        }
358        Strategy::SqlCommentInsertion => {
359            let text = std::str::from_utf8(payload).map_err(|_| EncodeError::InvalidUtf8)?;
360            Ok(sql_comment_insert(text))
361        }
362        Strategy::MysqlVersionedComment => {
363            let text = std::str::from_utf8(payload).map_err(|_| EncodeError::InvalidUtf8)?;
364            Ok(mysql_versioned_comment(
365                text,
366                MYSQL_VERSIONED_COMMENT_VERSION,
367            ))
368        }
369        Strategy::NullByte => Ok(null_byte_inject(payload)?),
370        Strategy::OverlongUtf8 => Ok(overlong_utf8(payload)?),
371        Strategy::OverlongUtf8More => Ok(overlong_utf8_more(payload)?),
372        Strategy::ChunkedSplit => {
373            let body = chunked_split(payload, CHUNKED_SPLIT_DEFAULT_CHUNK_SIZE)?.body;
374            String::from_utf8(body).map_err(|_| EncodeError::InvalidUtf8)
375        }
376        Strategy::ParameterPollution => Ok(parameter_pollute(payload)?),
377        Strategy::Base64Encode => Ok(base64_encode(payload)),
378        Strategy::Base64UrlEncode => Ok(base64_url_encode(payload)),
379        Strategy::HexEncode => Ok(hex_encode(payload)),
380        Strategy::Utf7Encode => {
381            let text = std::str::from_utf8(payload).map_err(|_| EncodeError::InvalidUtf8)?;
382            Ok(utf7_encode(text))
383        }
384        Strategy::GzipEncode => Ok(gzip_encode(payload)?),
385        Strategy::DeflateEncode => Ok(deflate_encode(payload)?),
386        Strategy::SpaceToComment => {
387            let text = std::str::from_utf8(payload).map_err(|_| EncodeError::InvalidUtf8)?;
388            Ok(space_to_comment(text))
389        }
390        Strategy::SpaceToDash => {
391            let text = std::str::from_utf8(payload).map_err(|_| EncodeError::InvalidUtf8)?;
392            Ok(space_to_dash(text))
393        }
394        Strategy::SpaceToHash => {
395            let text = std::str::from_utf8(payload).map_err(|_| EncodeError::InvalidUtf8)?;
396            Ok(space_to_hash(text))
397        }
398        Strategy::SpaceToPlus => {
399            let text = std::str::from_utf8(payload).map_err(|_| EncodeError::InvalidUtf8)?;
400            Ok(space_to_plus(text))
401        }
402        Strategy::SpaceToRandomBlank => {
403            let text = std::str::from_utf8(payload).map_err(|_| EncodeError::InvalidUtf8)?;
404            Ok(space_to_random_blank(text))
405        }
406        Strategy::PercentagePrefix => {
407            let text = std::str::from_utf8(payload).map_err(|_| EncodeError::InvalidUtf8)?;
408            Ok(percentage_prefix(text))
409        }
410        Strategy::BetweenObfuscation => {
411            let text = std::str::from_utf8(payload).map_err(|_| EncodeError::InvalidUtf8)?;
412            Ok(between_obfuscate(text))
413        }
414        Strategy::UnmagicQuotes => Ok(unmagic_quotes(payload)?),
415        Strategy::FullwidthEncode => {
416            let text = std::str::from_utf8(payload).map_err(|_| EncodeError::InvalidUtf8)?;
417            Ok(fullwidth_encode(text))
418        }
419        Strategy::HomoglyphEncode => {
420            let text = std::str::from_utf8(payload).map_err(|_| EncodeError::InvalidUtf8)?;
421            Ok(homoglyph_encode(text))
422        }
423        Strategy::TagCharEncode => {
424            let text = std::str::from_utf8(payload).map_err(|_| EncodeError::InvalidUtf8)?;
425            Ok(tag_char_encode(text))
426        }
427        Strategy::VariationSelectorPad => {
428            let text = std::str::from_utf8(payload).map_err(|_| EncodeError::InvalidUtf8)?;
429            Ok(variation_selector_pad(text, '\u{FE0F}'))
430        }
431        Strategy::VariationSelectorSupplementaryPad => {
432            let text = std::str::from_utf8(payload).map_err(|_| EncodeError::InvalidUtf8)?;
433            Ok(variation_selector_supplementary_pad(text))
434        }
435        Strategy::LigatureEncode => {
436            let text = std::str::from_utf8(payload).map_err(|_| EncodeError::InvalidUtf8)?;
437            Ok(ligature_encode(text))
438        }
439        Strategy::CircledLetterEncode => {
440            let text = std::str::from_utf8(payload).map_err(|_| EncodeError::InvalidUtf8)?;
441            Ok(circled_letter_encode(text))
442        }
443        Strategy::ParenthesizedLetterEncode => {
444            let text = std::str::from_utf8(payload).map_err(|_| EncodeError::InvalidUtf8)?;
445            Ok(parenthesized_letter_encode(text))
446        }
447        Strategy::SoftHyphenInject => {
448            let text = std::str::from_utf8(payload).map_err(|_| EncodeError::InvalidUtf8)?;
449            Ok(soft_hyphen_inject(text))
450        }
451        Strategy::WordJoinerWrap => {
452            let text = std::str::from_utf8(payload).map_err(|_| EncodeError::InvalidUtf8)?;
453            Ok(word_joiner_wrap(text))
454        }
455    }
456}
457
458/// All available strategies in escalation order (least aggressive → most aggressive).
459static ALL_STRATEGIES: std::sync::LazyLock<Vec<Strategy>> = std::sync::LazyLock::new(|| {
460    let mut strategies = vec![
461        Strategy::CaseAlternation,
462        Strategy::RandomCase,
463        Strategy::WhitespaceInsertion,
464        Strategy::SqlCommentInsertion,
465        Strategy::SpaceToPlus,
466        Strategy::SpaceToRandomBlank,
467        Strategy::SpaceToComment,
468        Strategy::SpaceToDash,
469        Strategy::SpaceToHash,
470        Strategy::UrlEncode,
471        Strategy::UrlEncodeLower,
472        Strategy::DoubleUrlEncode,
473        Strategy::UnicodeEncode,
474        Strategy::IisUnicodeEncode,
475        Strategy::JsonEncode,
476        Strategy::HtmlEntityEncode,
477        Strategy::HtmlEntityDecimalEncode,
478        Strategy::NullByte,
479        Strategy::PercentagePrefix,
480        Strategy::TripleUrlEncode,
481        Strategy::ChunkedSplit,
482        Strategy::ParameterPollution,
483        Strategy::MysqlVersionedComment,
484        Strategy::Base64Encode,
485        Strategy::Base64UrlEncode,
486        Strategy::OverlongUtf8,
487        Strategy::OverlongUtf8More,
488        Strategy::HexEncode,
489        Strategy::Utf7Encode,
490        Strategy::BetweenObfuscation,
491        Strategy::UnmagicQuotes,
492        Strategy::FullwidthEncode,
493        Strategy::HomoglyphEncode,
494        Strategy::GzipEncode,
495        Strategy::DeflateEncode,
496        Strategy::TagCharEncode,
497        Strategy::VariationSelectorPad,
498        Strategy::VariationSelectorSupplementaryPad,
499        Strategy::LigatureEncode,
500        Strategy::CircledLetterEncode,
501        Strategy::ParenthesizedLetterEncode,
502        Strategy::SoftHyphenInject,
503        Strategy::WordJoinerWrap,
504    ];
505    strategies.sort_by(|a, b| {
506        super::layered::aggressiveness(*a)
507            .partial_cmp(&super::layered::aggressiveness(*b))
508            .unwrap_or(std::cmp::Ordering::Equal)
509    });
510    strategies
511});
512
513#[must_use]
514pub fn all_strategies() -> &'static [Strategy] {
515    &ALL_STRATEGIES
516}
517
518#[cfg(test)]
519mod tests {
520    use super::*;
521
522    #[test]
523    fn encode_url_encode_basic() {
524        assert_eq!(encode("A<", Strategy::UrlEncode).unwrap(), "A%3C");
525    }
526
527    #[test]
528    fn encode_url_encode_lower() {
529        assert_eq!(encode("A<", Strategy::UrlEncodeLower).unwrap(), "A%3c");
530    }
531
532    #[test]
533    fn encode_double_url_encode() {
534        assert_eq!(
535            encode("A<", Strategy::DoubleUrlEncode).unwrap(),
536            "%2541%253C"
537        );
538    }
539
540    #[test]
541    fn encode_case_alternation() {
542        let result = encode("SELECT", Strategy::CaseAlternation).unwrap();
543        assert!(result.contains("SeL") || result.contains("sEl"));
544    }
545
546    #[test]
547    fn encode_null_byte() {
548        let result = encode("file.php", Strategy::NullByte).unwrap();
549        assert!(result.contains('\x00') || result.contains("%00"));
550    }
551
552    #[test]
553    fn encode_base64() {
554        assert_eq!(encode("hello", Strategy::Base64Encode).unwrap(), "aGVsbG8=");
555    }
556
557    #[test]
558    fn encode_hex() {
559        assert_eq!(encode("ABC", Strategy::HexEncode).unwrap(), "414243");
560    }
561
562    #[test]
563    fn encode_json() {
564        // F67: encoder now produces escaped CONTENT only, no
565        // surrounding quotes — the variant builder substitutes
566        // into an existing JSON string field.
567        assert_eq!(encode("A<", Strategy::JsonEncode).unwrap(), "A<");
568        // Real escape: backslash + control char.
569        assert_eq!(encode("a\\\nb", Strategy::JsonEncode).unwrap(), "a\\\\\\nb");
570    }
571
572    #[test]
573    fn encode_html_entity() {
574        assert_eq!(
575            encode("A<", Strategy::HtmlEntityEncode).unwrap(),
576            "&#x41;&#x3C;"
577        );
578    }
579
580    #[test]
581    fn encode_invalid_utf8_fails() {
582        let invalid = vec![0x80, 0x81, 0x82];
583        let result = encode(&invalid, Strategy::CaseAlternation);
584        assert!(matches!(result, Err(EncodeError::InvalidUtf8)));
585    }
586
587    #[test]
588    fn encode_payload_too_large_fails() {
589        let huge = vec![b'X'; MAX_PAYLOAD_SIZE + 1];
590        let result = encode(&huge, Strategy::UrlEncode);
591        assert!(matches!(result, Err(EncodeError::PayloadTooLarge { .. })));
592    }
593
594    /// R55 pass-19 I6 (CLAUDE.md §12 TESTING boundary): the gate is
595    /// `payload.len() > MAX_PAYLOAD_SIZE` (strictly greater-than), so
596    /// a payload of exactly `MAX_PAYLOAD_SIZE` MUST succeed. Anti-rig:
597    /// if someone changes the comparison to `>=`, this test fails
598    /// instantly. The complementary `> MAX_PAYLOAD_SIZE + 1` case is
599    /// pinned by `encode_payload_too_large_fails`.
600    #[test]
601    fn encode_at_exact_max_payload_size_succeeds() {
602        let at_limit = vec![b'X'; MAX_PAYLOAD_SIZE];
603        let result = encode(&at_limit, Strategy::UrlEncode);
604        assert!(
605            result.is_ok(),
606            "boundary contract: exactly MAX_PAYLOAD_SIZE bytes must encode, got {result:?}"
607        );
608    }
609
610    #[test]
611    fn all_strategies_non_empty() {
612        let strategies = all_strategies();
613        assert!(!strategies.is_empty());
614        assert!(strategies.contains(&Strategy::UrlEncode));
615    }
616
617    #[test]
618    fn strategy_as_str_roundtrip() {
619        for s in all_strategies() {
620            assert!(!s.as_str().is_empty());
621        }
622    }
623
624    #[test]
625    fn strategy_contexts_returns_slice() {
626        assert!(Strategy::UrlEncode.contexts().is_empty());
627        assert_eq!(Strategy::JsonEncode.contexts(), &["json"]);
628        assert_eq!(Strategy::SpaceToComment.contexts(), &["sql"]);
629    }
630
631    #[test]
632    fn encode_empty_payload() {
633        assert_eq!(encode("", Strategy::UrlEncode).unwrap(), "");
634    }
635
636    #[test]
637    fn encode_unicode() {
638        let result = encode("A<", Strategy::UnicodeEncode).unwrap();
639        assert!(result.contains("\\u"));
640    }
641
642    #[test]
643    fn encode_chunked_split() {
644        let result = encode("hello", Strategy::ChunkedSplit).unwrap();
645        assert!(result.contains("\r\n"));
646        assert!(result.ends_with("0\r\n\r\n"));
647    }
648
649    #[test]
650    fn encode_parameter_pollution() {
651        let result = encode("key=value", Strategy::ParameterPollution).unwrap();
652        assert!(result.contains("key="));
653    }
654
655    #[test]
656    fn encode_gzip_produces_base64() {
657        let result = encode("hello", Strategy::GzipEncode).unwrap();
658        // Gzip output is base64-encoded
659        assert!(!result.is_empty());
660    }
661
662    #[test]
663    fn encode_iis_unicode() {
664        let result = encode("A<", Strategy::IisUnicodeEncode).unwrap();
665        assert!(result.contains("%u"));
666    }
667}