1use std::collections::HashSet;
29
30pub const PAD_KEY: &str = "_wafrift_pad";
33
34pub const MIN_USEFUL_PAD: usize = 4 * 1024;
37
38pub const MAX_USEFUL_PAD: usize = 8 * 1024 * 1024;
43
44fn fill(n: usize) -> Vec<u8> {
64 fill_with_seed(n, process_nonce())
65}
66
67fn fill_with_seed(n: usize, extra_seed: u64) -> Vec<u8> {
68 const ALPHABET: &[u8] = b"abcdefghijklmnopqrstuvwxyz0123456789";
69 let mut v = Vec::with_capacity(n);
70 let mut state: u64 = 0x9E37_79B9_7F4A_7C15u64
72 .wrapping_add(n as u64)
73 .wrapping_add(extra_seed)
74 .wrapping_mul(0xBF58_476D_1CE4_E5B9);
75 if state == 0 {
76 state = 0xDEAD_BEEF_CAFE_F00D;
78 }
79 for _ in 0..n {
80 state ^= state << 13;
81 state ^= state >> 7;
82 state ^= state << 17;
83 v.push(ALPHABET[(state as usize) % ALPHABET.len()]);
84 }
85 v
86}
87
88fn process_nonce() -> u64 {
93 #[cfg(test)]
94 {
95 0
96 }
97 #[cfg(not(test))]
98 {
99 use std::sync::OnceLock;
100 static NONCE: OnceLock<u64> = OnceLock::new();
101 *NONCE.get_or_init(|| {
102 use rand::RngCore;
103 let mut rng = rand::rngs::OsRng;
104 rng.next_u64()
105 })
106 }
107}
108
109#[derive(Debug, Clone, PartialEq, Eq)]
111pub enum PadOutcome {
112 Padded { bytes: Vec<u8>, added: usize },
115 SkippedOpaque,
119 SkippedTooSmall,
121}
122
123pub fn pad(body: &[u8], content_type: &str, requested_bytes: usize) -> PadOutcome {
134 if requested_bytes < MIN_USEFUL_PAD {
135 return PadOutcome::SkippedTooSmall;
136 }
137 let requested_bytes = requested_bytes.min(MAX_USEFUL_PAD);
141
142 let ct_lower = content_type.to_ascii_lowercase();
143 let main_type = ct_lower.split(';').next().unwrap_or("").trim().to_string();
144
145 if main_type == "application/json" || main_type.ends_with("+json") {
146 return pad_json(body, requested_bytes);
147 }
148 if main_type == "application/x-www-form-urlencoded" {
149 return pad_form(body, requested_bytes);
150 }
151 if main_type == "multipart/form-data" {
152 if let Some(boundary) = extract_boundary(content_type) {
156 return pad_multipart(body, &boundary, requested_bytes);
157 }
158 return PadOutcome::SkippedOpaque;
161 }
162 if main_type.starts_with("text/") || main_type == "application/xml" {
163 if body.is_empty() {
168 return pad_form(body, requested_bytes);
169 }
170 return PadOutcome::SkippedOpaque;
171 }
172
173 PadOutcome::SkippedOpaque
174}
175
176fn pad_json(body: &[u8], requested_bytes: usize) -> PadOutcome {
177 if body.len() > MAX_USEFUL_PAD {
186 return PadOutcome::SkippedOpaque;
187 }
188 let pad = fill(requested_bytes);
189 let pad_str = String::from_utf8(pad).expect("fill produces ASCII-only bytes");
203 if body.is_empty() {
204 let new_body = format!("{{\"{PAD_KEY}\":\"{pad_str}\"}}").into_bytes();
205 return PadOutcome::Padded {
206 bytes: new_body,
207 added: requested_bytes,
208 };
209 }
210 if let Ok(s) = std::str::from_utf8(body)
211 && let Ok(serde_json::Value::Object(map)) = serde_json::from_str::<serde_json::Value>(s)
212 {
213 if let Some(open) = s.find('{') {
223 let pad_key: String = if map.contains_key(PAD_KEY) {
233 let mut suffix = 1u32;
234 loop {
235 let candidate = format!("{PAD_KEY}_{suffix}");
236 if !map.contains_key(&candidate) {
237 break candidate;
238 }
239 suffix += 1;
240 if suffix == u32::MAX {
246 break PAD_KEY.to_string();
247 }
248 }
249 } else {
250 PAD_KEY.to_string()
251 };
252 let after = &s[open + 1..];
253 let glue = if after.trim_start().starts_with('}') {
256 ""
257 } else {
258 ","
259 };
260 let new_body = format!("{{\"{pad_key}\":\"{pad_str}\"{glue}{after}").into_bytes();
261 let added = new_body.len().saturating_sub(body.len());
262 return PadOutcome::Padded {
263 bytes: new_body,
264 added,
265 };
266 }
267 }
268 let Ok(original) = std::str::from_utf8(body) else {
270 return PadOutcome::SkippedOpaque;
271 };
272 let wrapped = if body.len() <= MAX_USEFUL_PAD
275 && serde_json::from_slice::<serde_json::Value>(body).is_ok()
276 {
277 format!("{{\"{PAD_KEY}\":\"{pad_str}\",\"payload\":{original}}}")
278 } else {
279 let escaped = serde_json::to_string(&original).unwrap_or_else(|_| "\"\"".into());
281 format!("{{\"{PAD_KEY}\":\"{pad_str}\",\"payload\":{escaped}}}")
282 };
283 let new_body = wrapped.into_bytes();
284 let added = new_body.len().saturating_sub(body.len());
285 PadOutcome::Padded {
286 bytes: new_body,
287 added,
288 }
289}
290
291fn pad_form(body: &[u8], requested_bytes: usize) -> PadOutcome {
292 let pad = fill(requested_bytes);
293 let pad_str = String::from_utf8(pad).expect("fill produces ASCII-only bytes");
294 let new_body = if body.is_empty() {
295 format!("{PAD_KEY}={pad_str}").into_bytes()
296 } else {
297 let mut out = Vec::with_capacity(body.len() + requested_bytes + 32);
298 out.extend_from_slice(format!("{PAD_KEY}={pad_str}&").as_bytes());
299 out.extend_from_slice(body);
300 out
301 };
302 let added = new_body.len().saturating_sub(body.len());
303 PadOutcome::Padded {
304 bytes: new_body,
305 added,
306 }
307}
308
309fn pad_multipart(body: &[u8], boundary: &str, requested_bytes: usize) -> PadOutcome {
310 let prefix = format!("--{boundary}");
319 let body_str = std::str::from_utf8(body).unwrap_or("");
320 if !body.is_empty() && !body_str.starts_with(&prefix) {
321 return PadOutcome::SkippedOpaque;
322 }
323 let pad = fill(requested_bytes);
324 let mut leading = Vec::with_capacity(requested_bytes + boundary.len() + 128);
325 leading.extend_from_slice(format!("--{boundary}\r\n").as_bytes());
326 leading.extend_from_slice(
327 format!("Content-Disposition: form-data; name=\"{PAD_KEY}\"\r\n").as_bytes(),
328 );
329 leading.extend_from_slice(b"\r\n");
330 leading.extend_from_slice(&pad);
331 leading.extend_from_slice(b"\r\n");
332 let mut new_body = Vec::with_capacity(leading.len() + body.len());
333 new_body.extend_from_slice(&leading);
334 new_body.extend_from_slice(body);
335 let added = new_body.len().saturating_sub(body.len());
336 PadOutcome::Padded {
337 bytes: new_body,
338 added,
339 }
340}
341
342fn extract_boundary(content_type: &str) -> Option<String> {
343 for part in content_type.split(';') {
344 let p = part.trim();
345 let rest = p
350 .strip_prefix("boundary=")
351 .or_else(|| p.strip_prefix("Boundary="))
352 .or_else(|| p.strip_prefix("BOUNDARY="))
353 .or_else(|| {
354 match p.get(..9) {
360 Some(h) if h.eq_ignore_ascii_case("boundary=") => p.get(9..),
361 _ => None,
362 }
363 });
364 if let Some(rest) = rest {
365 let trimmed = rest.trim_matches('"').trim();
366 if !trimmed.is_empty() {
367 return Some(trimmed.to_string());
368 }
369 }
370 }
371 None
372}
373
374#[must_use]
377pub fn looks_padded(body: &[u8]) -> bool {
378 let needle = format!("\"{PAD_KEY}\"").into_bytes();
379 let needle_form = format!("{PAD_KEY}=").into_bytes();
380 let needle_mp = format!("name=\"{PAD_KEY}\"").into_bytes();
381 [needle, needle_form, needle_mp]
382 .iter()
383 .any(|n| memchr_subslice(body, n))
384}
385
386fn memchr_subslice(haystack: &[u8], needle: &[u8]) -> bool {
387 if needle.is_empty() || needle.len() > haystack.len() {
388 return false;
389 }
390 haystack.windows(needle.len()).any(|w| w == needle)
391}
392
393#[must_use]
396pub fn known_thresholds() -> Vec<(&'static str, usize)> {
397 vec![
398 ("cloudflare-free", 128 * 1024),
399 ("cloudflare-pro", 8 * 1024),
400 ("cloudflare-business", 8 * 1024),
401 ("cloudflare-enterprise", 128 * 1024),
402 ("aws-waf-default", 8 * 1024),
403 ("aws-waf-classic", 8 * 1024),
404 ("aws-waf-extended", 64 * 1024),
405 ("akamai-default", 8 * 1024),
406 ("imperva-default", 128 * 1024),
407 ("modsecurity-default", 128 * 1024),
408 ("naxsi-default", 65 * 1024),
409 ]
410}
411
412#[must_use]
415pub fn known_threshold_values() -> HashSet<usize> {
416 known_thresholds().into_iter().map(|(_, v)| v).collect()
417}
418
419#[cfg(test)]
420mod tests {
421 use super::*;
422
423 #[test]
424 fn fill_is_deterministic_and_inert() {
425 let v = fill(8 * 1024);
426 assert_eq!(v.len(), 8 * 1024);
427 for &b in &v {
429 assert!(
430 (b.is_ascii_lowercase() || b.is_ascii_digit()),
431 "byte {b:#x} ({}) outside [a-z0-9]",
432 b as char
433 );
434 }
435 assert_eq!(fill(8 * 1024), v);
437 }
438
439 #[test]
440 fn fill_no_long_runs() {
441 let v = fill(64 * 1024);
447 let mut max_run = 1usize;
448 let mut cur_run = 1usize;
449 for w in v.windows(2) {
450 if w[0] == w[1] {
451 cur_run += 1;
452 max_run = max_run.max(cur_run);
453 } else {
454 cur_run = 1;
455 }
456 }
457 assert!(
458 max_run <= 6,
459 "filler has a run of {max_run} same bytes — would trigger WAF run-detection"
460 );
461 }
462
463 #[test]
464 fn pathological_size_clamps_to_max() {
465 let out = pad(b"id=42", "application/x-www-form-urlencoded", usize::MAX);
468 let PadOutcome::Padded { bytes, .. } = out else {
469 panic!("expected Padded, got {out:?}");
470 };
471 assert!(bytes.len() <= MAX_USEFUL_PAD + 64);
473 assert!(bytes.len() >= MAX_USEFUL_PAD);
474 }
475
476 #[test]
477 fn malformed_content_type_is_safe() {
478 for ct in &[
480 "",
481 "////",
482 ";;;;",
483 "application/json;;;boundary=",
484 "\x00\x01\x02",
485 ] {
486 let _ = pad(b"id=42", ct, 8 * 1024);
488 }
489 }
490
491 #[test]
492 fn empty_input_with_huge_size() {
493 let out = pad(b"", "application/json", 1024 * 1024);
496 let PadOutcome::Padded { bytes, .. } = out else {
497 panic!()
498 };
499 let _: serde_json::Value = serde_json::from_slice(&bytes).expect("valid json");
501 }
502
503 #[test]
504 fn fill_distinct_per_size() {
505 let a = fill(8 * 1024);
509 let b = fill(8 * 1024 + 1);
510 assert_ne!(&a[..32], &b[..32]);
511 }
512
513 #[test]
514 fn skip_too_small() {
515 assert_eq!(
516 pad(b"x", "application/json", 100),
517 PadOutcome::SkippedTooSmall
518 );
519 }
520
521 #[test]
522 fn json_object_preserves_payload() {
523 let body = br#"{"q":"' OR 1=1--"}"#;
524 let out = pad(body, "application/json", 8 * 1024);
525 let PadOutcome::Padded { bytes, added } = out else {
526 panic!("expected padded, got {out:?}");
527 };
528 assert!(added >= 8 * 1024, "added={added}");
529 let v: serde_json::Value = serde_json::from_slice(&bytes).expect("valid json");
531 assert_eq!(v["_wafrift_pad"].as_str().map(str::len), Some(8 * 1024));
532 assert_eq!(v["q"].as_str(), Some("' OR 1=1--"));
533 assert!(looks_padded(&bytes));
534 }
535
536 #[test]
537 fn json_empty_body_emits_object() {
538 let out = pad(b"", "application/json", 8 * 1024);
539 let PadOutcome::Padded { bytes, .. } = out else {
540 panic!()
541 };
542 let v: serde_json::Value = serde_json::from_slice(&bytes).expect("valid json");
543 assert!(v.is_object());
544 assert!(v["_wafrift_pad"].is_string());
545 }
546
547 #[test]
548 fn json_array_root_wrapped_with_payload() {
549 let out = pad(br#"["x","y"]"#, "application/json", 8 * 1024);
550 let PadOutcome::Padded { bytes, .. } = out else {
551 panic!()
552 };
553 let v: serde_json::Value = serde_json::from_slice(&bytes).expect("valid json");
554 assert!(v["_wafrift_pad"].is_string());
555 assert!(v["payload"].is_array());
556 assert_eq!(v["payload"][0].as_str(), Some("x"));
557 }
558
559 #[test]
560 fn json_with_charset_param() {
561 let out = pad(br#"{"a":1}"#, "application/json; charset=utf-8", 8 * 1024);
562 assert!(matches!(out, PadOutcome::Padded { .. }));
563 }
564
565 #[test]
566 fn json_plus_suffix() {
567 let out = pad(br#"{"a":1}"#, "application/vnd.foo+json", 8 * 1024);
568 assert!(matches!(out, PadOutcome::Padded { .. }));
569 }
570
571 #[test]
572 fn form_prepends_padding_then_original() {
573 let body = b"username=admin&password=' OR 1=1--";
574 let out = pad(body, "application/x-www-form-urlencoded", 16 * 1024);
575 let PadOutcome::Padded { bytes, added } = out else {
576 panic!()
577 };
578 assert!(added >= 16 * 1024, "added={added}");
579 assert!(bytes.starts_with(b"_wafrift_pad="));
580 assert!(memchr_subslice(&bytes, body));
582 }
583
584 #[test]
585 fn multipart_splices_in_leading_part() {
586 let boundary = "----WebKitFormBoundary123";
587 let body = format!(
588 "--{boundary}\r\n\
589 Content-Disposition: form-data; name=\"q\"\r\n\
590 \r\n' OR 1=1--\r\n\
591 --{boundary}--\r\n"
592 );
593 let ct = format!("multipart/form-data; boundary={boundary}");
594 let out = pad(body.as_bytes(), &ct, 16 * 1024);
595 let PadOutcome::Padded { bytes, .. } = out else {
596 panic!()
597 };
598 let s = std::str::from_utf8(&bytes).unwrap();
599 assert!(s.starts_with(&format!("--{boundary}\r\n")));
601 assert!(s.contains("name=\"_wafrift_pad\""));
602 assert!(s.contains("' OR 1=1--"));
604 let boundary_count = s.matches(&format!("--{boundary}")).count();
607 assert!(boundary_count >= 3, "boundary_count={boundary_count}");
608 }
609
610 #[test]
611 fn multipart_without_boundary_skipped() {
612 let out = pad(b"some body", "multipart/form-data", 16 * 1024);
613 assert_eq!(out, PadOutcome::SkippedOpaque);
614 }
615
616 #[test]
617 fn multipart_with_quoted_boundary() {
618 let boundary = "abc123";
619 let body = format!("--{boundary}\r\n\r\n--{boundary}--\r\n");
620 let out = pad(
621 body.as_bytes(),
622 &format!("multipart/form-data; boundary=\"{boundary}\""),
623 16 * 1024,
624 );
625 assert!(matches!(out, PadOutcome::Padded { .. }));
626 }
627
628 #[test]
629 fn opaque_binary_skipped() {
630 let body = b"\x89PNG\r\n\x1a\n\x00\x00";
631 let out = pad(body, "image/png", 16 * 1024);
632 assert_eq!(out, PadOutcome::SkippedOpaque);
633 }
634
635 #[test]
636 fn known_thresholds_includes_aws_and_cloudflare() {
637 let names: Vec<_> = known_thresholds().iter().map(|(n, _)| *n).collect();
638 assert!(names.iter().any(|n| n.starts_with("cloudflare")));
639 assert!(names.iter().any(|n| n.starts_with("aws-waf")));
640 }
641
642 #[test]
643 fn looks_padded_detects_each_shape() {
644 let json = pad(b"{}", "application/json", 8 * 1024);
645 let form = pad(b"", "application/x-www-form-urlencoded", 8 * 1024);
646 if let PadOutcome::Padded { bytes, .. } = json {
647 assert!(looks_padded(&bytes));
648 }
649 if let PadOutcome::Padded { bytes, .. } = form {
650 assert!(looks_padded(&bytes));
651 }
652 assert!(!looks_padded(b"plain old body"));
653 }
654
655 #[test]
656 fn oversized_json_body_does_not_oom() {
657 let huge = "x".repeat(MAX_USEFUL_PAD + 1024);
660 let body = format!("[{huge}]");
661 let out = pad(body.as_bytes(), "application/json", 8 * 1024);
662 assert!(
664 matches!(out, PadOutcome::SkippedOpaque | PadOutcome::SkippedTooSmall),
665 "oversized JSON body should be skipped, got {out:?}"
666 );
667 }
668
669 #[test]
670 fn json_body_with_existing_pad_key_does_not_collide() {
671 let body = format!(r#"{{"{PAD_KEY}":"attacker-controlled","payload":"x"}}"#);
679 let out = pad(body.as_bytes(), "application/json", 8 * 1024);
680 let bytes = match out {
681 PadOutcome::Padded { bytes, .. } => bytes,
682 other => panic!("expected Padded, got {other:?}"),
683 };
684 let s = std::str::from_utf8(&bytes).unwrap();
685 let parsed: serde_json::Map<String, serde_json::Value> = serde_json::from_str(s).unwrap();
687 assert!(
688 parsed.contains_key(PAD_KEY),
689 "original PAD_KEY must survive: {s}"
690 );
691 let injected_key_count = parsed
693 .keys()
694 .filter(|k| k.starts_with(PAD_KEY) && k.as_str() != PAD_KEY)
695 .count();
696 assert!(
697 injected_key_count >= 1,
698 "must inject a non-colliding pad key: {s}"
699 );
700 assert_eq!(parsed.get("payload").and_then(|v| v.as_str()), Some("x"));
702 assert_eq!(
704 parsed.get(PAD_KEY).and_then(|v| v.as_str()),
705 Some("attacker-controlled")
706 );
707 }
708
709 #[test]
710 fn fill_with_seed_varies_across_seeds() {
711 let a = fill_with_seed(256, 0xAAAA_AAAA);
715 let b = fill_with_seed(256, 0xBBBB_BBBB);
716 assert_ne!(a, b, "different seeds must produce different output");
717 assert_eq!(a, fill_with_seed(256, 0xAAAA_AAAA));
719 }
720
721 #[test]
724 fn fill_zero_returns_empty() {
725 let v = fill(0);
726 assert!(v.is_empty(), "fill(0) must return empty vec");
727 }
728
729 #[test]
730 fn fill_with_seed_zero_n_returns_empty() {
731 let v = fill_with_seed(0, 0xDEAD);
732 assert!(v.is_empty());
733 }
734
735 #[test]
738 fn text_xml_nonempty_body_returns_skipped_opaque() {
739 let xml_body = b"<?xml version=\"1.0\"?><root><elem>value</elem></root>";
740 let out = pad(xml_body, "text/xml", 8 * 1024);
741 assert_eq!(
742 out,
743 PadOutcome::SkippedOpaque,
744 "non-empty text/xml body must not be padded — would corrupt XML structure"
745 );
746 }
747
748 #[test]
749 fn application_xml_nonempty_body_returns_skipped_opaque() {
750 let xml_body = b"<Envelope><Body><req/></Body></Envelope>";
751 let out = pad(xml_body, "application/xml", 8 * 1024);
752 assert_eq!(
753 out,
754 PadOutcome::SkippedOpaque,
755 "non-empty application/xml body must be SkippedOpaque"
756 );
757 }
758
759 #[test]
762 fn text_xml_empty_body_applies_form_padding() {
763 let out = pad(b"", "text/xml", 8 * 1024);
766 let PadOutcome::Padded { bytes, added } = out else {
767 panic!("empty text/xml must produce Padded, got {out:?}");
768 };
769 assert!(added >= 8 * 1024, "added={added}");
770 assert!(
771 bytes.starts_with(b"_wafrift_pad="),
772 "empty text/xml padding must use form-key prefix"
773 );
774 }
775
776 #[test]
777 fn application_xml_empty_body_applies_form_padding() {
778 let out = pad(b"", "application/xml", 8 * 1024);
779 assert!(
780 matches!(out, PadOutcome::Padded { .. }),
781 "empty application/xml must produce Padded"
782 );
783 }
784
785 #[test]
788 fn text_plain_nonempty_body_returns_skipped_opaque() {
789 let out = pad(b"hello world", "text/plain", 8 * 1024);
790 assert_eq!(out, PadOutcome::SkippedOpaque);
791 }
792
793 #[test]
796 fn known_threshold_values_contains_expected_numbers() {
797 let values = known_threshold_values();
798 assert!(
800 values.contains(&(8 * 1024)),
801 "must include 8 KiB (cloudflare-pro / aws-waf)"
802 );
803 assert!(
804 values.contains(&(64 * 1024)),
805 "must include 64 KiB (aws-waf-extended)"
806 );
807 assert!(
808 values.contains(&(128 * 1024)),
809 "must include 128 KiB (cloudflare-enterprise / imperva / modsecurity)"
810 );
811 assert!(
812 values.contains(&(65 * 1024)),
813 "must include 65 KiB (naxsi-default)"
814 );
815 }
816
817 #[test]
818 fn known_threshold_values_matches_known_thresholds() {
819 let from_pairs: std::collections::HashSet<usize> =
820 known_thresholds().into_iter().map(|(_, v)| v).collect();
821 let from_fn = known_threshold_values();
822 assert_eq!(
823 from_pairs, from_fn,
824 "known_threshold_values() must match the values from known_thresholds()"
825 );
826 }
827
828 #[test]
831 fn extract_boundary_multibyte_at_byte_9_does_not_panic() {
832 let ct = "multipart/form-data; \u{2261}boundary=abc"; let boundary = extract_boundary(ct);
839 let _ = boundary; let ct2 = "multipart/form-data; boundary=\u{2261}abc"; let boundary2 = extract_boundary(ct2);
845 assert!(
847 boundary2.is_some(),
848 "unicode in boundary value must be preserved"
849 );
850 }
851
852 #[test]
853 fn extract_boundary_with_unicode_before_byte_9_does_not_panic() {
854 let ct = "multipart/form-data; bound\u{2261}y=myfence";
858 let _ = extract_boundary(ct); }
860
861 #[test]
864 fn pad_multipart_body_not_starting_with_boundary_is_skipped() {
865 let boundary = "abc123";
868 let malformed_body = b"this body does not start with the boundary";
869 let ct = format!("multipart/form-data; boundary={boundary}");
870 let out = pad(malformed_body, &ct, 16 * 1024);
871 assert_eq!(
872 out,
873 PadOutcome::SkippedOpaque,
874 "malformed multipart (body missing leading boundary) must be SkippedOpaque"
875 );
876 }
877
878 #[test]
881 fn looks_padded_detects_multipart_shape() {
882 let boundary = "fence42";
883 let body = format!("--{boundary}\r\n\r\n--{boundary}--\r\n");
884 let ct = format!("multipart/form-data; boundary={boundary}");
885 let out = pad(body.as_bytes(), &ct, 8 * 1024);
886 if let PadOutcome::Padded { bytes, .. } = out {
887 assert!(
888 looks_padded(&bytes),
889 "looks_padded must detect multipart padding"
890 );
891 }
892 }
893
894 #[test]
897 fn min_useful_pad_is_4_kib() {
898 assert_eq!(MIN_USEFUL_PAD, 4 * 1024, "MIN_USEFUL_PAD must be 4 KiB");
899 }
900
901 #[test]
902 fn max_useful_pad_is_8_mib() {
903 assert_eq!(
904 MAX_USEFUL_PAD,
905 8 * 1024 * 1024,
906 "MAX_USEFUL_PAD must be 8 MiB"
907 );
908 }
909
910 #[test]
911 fn pad_at_exactly_min_useful_pad_produces_padded() {
912 let out = pad(b"", "application/json", MIN_USEFUL_PAD);
915 assert!(
916 matches!(out, PadOutcome::Padded { .. }),
917 "exactly MIN_USEFUL_PAD must produce Padded, not SkippedTooSmall"
918 );
919 }
920
921 #[test]
922 fn pad_one_below_min_useful_pad_is_too_small() {
923 let out = pad(b"", "application/json", MIN_USEFUL_PAD - 1);
924 assert_eq!(
925 out,
926 PadOutcome::SkippedTooSmall,
927 "one byte below MIN_USEFUL_PAD must be SkippedTooSmall"
928 );
929 }
930}