1use std::io::{
2 self,
3 Read,
4};
5use std::str;
6
7use anyhow::Result;
8use brotli::Decompressor;
9use flate2::bufread::GzDecoder;
10use httparse::{
11 EMPTY_HEADER,
12 Header,
13 Request as HttpRequest,
14 Response as HttpResponse,
15 Status as ParseStatus,
16};
17use tokio::task;
18use tracing::debug;
19
20const MAX_HEADERS: usize = 64;
21const MAX_CHUNK_SIZE: usize = 100 * 1024 * 1024;
24
25pub(crate) struct GzipMembers<'a> {
32 member: GzDecoder<&'a [u8]>,
33 first_member: bool,
34}
35
36impl<'a> GzipMembers<'a> {
37 pub(crate) fn new(data: &'a [u8]) -> Self {
38 Self {
39 member: GzDecoder::new(data),
40 first_member: true,
41 }
42 }
43}
44
45impl Read for GzipMembers<'_> {
46 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
47 loop {
48 let read = match self.member.read(buf) {
49 Ok(read) => read,
50 Err(_) if !self.first_member => return Ok(0),
51 Err(e) => return Err(e),
52 };
53 let rest = *self.member.get_ref();
54 if read > 0 || buf.is_empty() || !rest.starts_with(&[0x1f, 0x8b]) {
55 return Ok(read);
56 }
57 self.member = GzDecoder::new(rest);
58 self.first_member = false;
59 }
60 }
61}
62
63#[derive(Debug, PartialEq)]
64enum ContentCategory {
65 Empty,
66 Binary,
67 Json,
68 JavaScript,
69 Css,
70 Html,
71 Xml,
72 Svg,
73 Text,
74 Font,
75}
76
77lazy_static::lazy_static! {
78 static ref HEADERS_CACHE: dashmap::DashMap<&'static str, &'static str> = {
79 let map = dashmap::DashMap::new();
80 map.insert("content-type", "content-type");
81 map.insert("content-length", "content-length");
82 map.insert("content-encoding", "content-encoding");
83 map.insert("transfer-encoding", "transfer-encoding");
84 map
85 };
86}
87
88#[derive(Debug)]
89pub enum ChunkParseResult<'a> {
90 Complete {
91 chunk_data: &'a [u8],
92 next_pos: usize,
93 },
94 EndOfChunks {
95 next_pos: usize,
96 },
97 Incomplete {
98 chunk_data: Option<&'a [u8]>,
99 consumed: usize,
100 },
101 Skip {
102 consumed: usize,
103 },
104}
105
106pub type RequestParseResult<'a> = (
107 Option<&'a str>,
108 Option<&'a str>,
109 Option<u16>,
110 Vec<Header<'a>>,
111);
112
113pub struct RequestParser;
114
115impl RequestParser {
116 pub fn parse(buffer: &[u8]) -> Result<RequestParseResult<'_>> {
117 let mut headers = [EMPTY_HEADER; MAX_HEADERS];
118 let mut req = HttpRequest::new(&mut headers);
119
120 match req.parse(buffer)? {
121 ParseStatus::Complete(_) => Ok((
122 req.method,
123 req.path,
124 req.version.map(|v| v as u16),
125 req.headers.to_vec(),
126 )),
127 ParseStatus::Partial => Ok((None, None, None, Vec::new())),
128 }
129 }
130
131 pub fn get_content_length(headers: &[Header<'_>]) -> usize {
132 if let Some(content_length) = HEADERS_CACHE.get("content-length") {
133 let header_name = content_length.value();
134 for h in headers {
135 if h.name.eq_ignore_ascii_case(header_name) {
136 if let Ok(v) = str::from_utf8(h.value)
137 && let Ok(len) = v.parse::<usize>()
138 {
139 return len;
140 }
141 break;
142 }
143 }
144 }
145 0
146 }
147
148 pub fn get_content_encoding<'a>(headers: &'a [Header<'_>]) -> Option<&'a str> {
149 for header in headers {
150 if header.name.eq_ignore_ascii_case("content-encoding")
151 && let Ok(value) = std::str::from_utf8(header.value)
152 {
153 return Some(value.trim());
154 }
155 }
156 None
157 }
158
159 pub fn is_gzip_encoded(headers: &[Header<'_>]) -> bool {
160 if let Some(encoding) = Self::get_content_encoding(headers) {
161 return encoding == "gzip" || encoding.eq_ignore_ascii_case("gzip");
162 }
163 false
164 }
165
166 pub fn is_brotli_encoded(headers: &[Header<'_>]) -> bool {
167 if let Some(encoding) = Self::get_content_encoding(headers) {
168 return encoding == "br" || encoding.eq_ignore_ascii_case("br");
169 }
170 false
171 }
172
173 pub fn is_chunked_transfer(headers: &[Header<'_>]) -> bool {
174 if let Some(transfer_encoding) = HEADERS_CACHE.get("transfer-encoding") {
175 let header_name = transfer_encoding.value();
176 for h in headers {
177 if h.name.eq_ignore_ascii_case(header_name)
178 && (h.value == b"chunked" || h.value.eq_ignore_ascii_case(b"chunked"))
179 {
180 return true;
181 }
182 }
183 }
184 false
185 }
186
187 pub fn extract_body(buffer: &[u8]) -> Option<&[u8]> {
188 buffer
189 .windows(4)
190 .position(|window| window == b"\r\n\r\n")
191 .map(|headers_end| {
192 let body_start = headers_end + 4;
193 &buffer[body_start..]
194 })
195 }
196
197 pub fn process_chunked_body(body: &[u8]) -> Vec<u8> {
198 if body.is_empty() {
199 return Vec::new();
200 }
201
202 let estimated_capacity = body.len() * 9 / 10;
203 let mut result = Vec::with_capacity(estimated_capacity);
204 let mut pos = 0;
205 let mut chunks_found = 0;
206 let mut incomplete_chunk = false;
207 let mut last_incomplete_data: Option<&[u8]> = None;
208 let mut last_chunk_size: Option<usize> = None;
209
210 while pos < body.len() {
211 match Self::parse_chunk(&body[pos..], &mut chunks_found) {
212 Ok(ChunkParseResult::Complete {
213 chunk_data,
214 next_pos,
215 }) => {
216 result.extend_from_slice(chunk_data);
217 pos += next_pos;
218 last_incomplete_data = None;
219 last_chunk_size = None;
220 }
221 Ok(ChunkParseResult::EndOfChunks { next_pos }) => {
222 pos += next_pos;
223 if pos < body.len() {
224 debug!(
225 "Found end marker with {} bytes of trailing data",
226 body.len() - pos
227 );
228 }
229 break;
230 }
231 Ok(ChunkParseResult::Incomplete {
232 chunk_data,
233 consumed,
234 }) => {
235 if let Some(data) = chunk_data {
236 last_incomplete_data = Some(data);
237 if let Some(chunk_line_end) = data.windows(2).position(|w| w == b"\r\n")
238 && let Ok(size_str) = std::str::from_utf8(&data[..chunk_line_end])
239 && let Ok(size) = usize::from_str_radix(size_str.trim(), 16)
240 {
241 last_chunk_size = Some(size);
242 }
243 result.extend_from_slice(data);
244 }
245 pos += consumed;
246 incomplete_chunk = true;
247
248 if pos >= body.len() {
249 break;
250 }
251 }
252 Ok(ChunkParseResult::Skip { consumed }) => {
253 pos += consumed;
254 }
255 Err(e) => {
256 debug!("Chunk parsing error: {:?} at position {}", e, pos);
257
258 if pos < body.len() && body.len() - pos > 4 {
259 if incomplete_chunk {
260 if let (Some(last_incomplete_data), Some(last_chunk_size)) =
261 (last_incomplete_data, last_chunk_size)
262 {
263 let remaining = &body[pos..];
264 let last_size = last_chunk_size;
265 let last_data_len = last_incomplete_data.len();
266
267 if last_data_len + remaining.len() >= last_size {
268 debug!("Adding remaining data to complete partial chunk");
269 result.extend_from_slice(remaining);
270 }
271 } else {
272 result.extend_from_slice(&body[pos..]);
273 }
274 } else {
275 result.extend_from_slice(&body[pos..]);
276 }
277 }
278 break;
279 }
280 }
281 }
282
283 if chunks_found > 0 && !result.is_empty() && !incomplete_chunk {
284 return result;
285 }
286
287 Self::handle_chunked_edge_cases(body, result, chunks_found, incomplete_chunk)
288 }
289
290 fn parse_chunk<'a>(
291 chunk_data: &'a [u8], chunks_found: &mut usize,
292 ) -> Result<ChunkParseResult<'a>> {
293 let line_end = match chunk_data.windows(2).position(|w| w == b"\r\n") {
294 Some(pos) => pos,
295 None => return Err(anyhow::anyhow!("No CRLF found in chunk header")),
296 };
297
298 if line_end == 0 {
299 return Ok(ChunkParseResult::Skip { consumed: 2 });
300 }
301
302 let hex_str = match std::str::from_utf8(&chunk_data[..line_end]) {
303 Ok(s) => s.trim(),
304 Err(_) => {
305 return Ok(ChunkParseResult::Skip {
306 consumed: line_end + 2,
307 });
308 }
309 };
310
311 if hex_str.is_empty() {
312 return Ok(ChunkParseResult::Skip {
313 consumed: line_end + 2,
314 });
315 }
316
317 let size_part = if let Some(semi_idx) = hex_str.find(';') {
318 &hex_str[..semi_idx]
319 } else {
320 hex_str
321 };
322
323 let chunk_size = match usize::from_str_radix(size_part, 16) {
324 Ok(size) if size <= MAX_CHUNK_SIZE => size,
325 Ok(_) => {
326 return Ok(ChunkParseResult::Skip {
327 consumed: line_end + 2,
328 });
329 }
330 Err(_) => {
331 return Ok(ChunkParseResult::Skip {
332 consumed: line_end + 2,
333 });
334 }
335 };
336
337 *chunks_found += 1;
338
339 if chunk_size == 0 {
340 return Ok(ChunkParseResult::EndOfChunks {
341 next_pos: line_end + 2 + 2,
342 });
343 }
344
345 let chunk_start = line_end + 2;
346
347 let Some(chunk_end) = chunk_start.checked_add(chunk_size) else {
348 return Ok(ChunkParseResult::Skip {
349 consumed: line_end + 2,
350 });
351 };
352
353 if chunk_end <= chunk_data.len() {
354 let Some(next_pos) = chunk_end.checked_add(2) else {
355 return Ok(ChunkParseResult::Incomplete {
356 chunk_data: Some(&chunk_data[chunk_start..chunk_end]),
357 consumed: chunk_data.len(),
358 });
359 };
360 if next_pos <= chunk_data.len() {
361 Ok(ChunkParseResult::Complete {
362 chunk_data: &chunk_data[chunk_start..chunk_end],
363 next_pos,
364 })
365 } else {
366 Ok(ChunkParseResult::Incomplete {
367 chunk_data: Some(&chunk_data[chunk_start..chunk_end]),
368 consumed: chunk_data.len(),
369 })
370 }
371 } else if chunk_start < chunk_data.len() {
372 Ok(ChunkParseResult::Incomplete {
373 chunk_data: Some(&chunk_data[chunk_start..]),
374 consumed: chunk_data.len(),
375 })
376 } else {
377 Ok(ChunkParseResult::Incomplete {
378 chunk_data: None,
379 consumed: chunk_data.len(),
380 })
381 }
382 }
383
384 fn handle_chunked_edge_cases(
385 body: &[u8], result: Vec<u8>, chunks_found: usize, incomplete_chunk: bool,
386 ) -> Vec<u8> {
387 if chunks_found == 0 && result.is_empty() && !body.is_empty() {
388 return body.to_vec();
389 }
390
391 if incomplete_chunk && body.len() > result.len() {
392 let mut combined = result;
393
394 if combined.len() < body.len() {
395 let unparsed_start = body
396 .iter()
397 .zip(combined.iter())
398 .position(|(a, b)| a != b)
399 .unwrap_or(combined.len().min(body.len()));
400
401 if unparsed_start < body.len() {
402 combined.extend_from_slice(&body[unparsed_start..]);
403 }
404 }
405
406 return combined;
407 }
408
409 if result.is_empty() && !body.is_empty() {
410 body.to_vec()
411 } else {
412 result
413 }
414 }
415}
416
417pub struct ResponseParser;
418
419impl ResponseParser {
420 pub fn parse(buffer: &[u8]) -> Result<(Option<u16>, Vec<Header<'_>>)> {
421 let mut headers = [EMPTY_HEADER; MAX_HEADERS];
422 let mut res = HttpResponse::new(&mut headers);
423
424 match res.parse(buffer)? {
425 ParseStatus::Complete(_) => Ok((res.code, res.headers.to_vec())),
426 ParseStatus::Partial => Ok((None, Vec::new())),
427 }
428 }
429}
430
431pub struct BodyParser;
432
433impl BodyParser {
434 pub fn is_image(headers: &[Header<'_>]) -> bool {
435 headers
436 .iter()
437 .any(|h| h.name.eq_ignore_ascii_case("content-type") && h.value.starts_with(b"image/"))
438 }
439
440 pub fn get_content_encoding<'a>(headers: &'a [Header<'_>]) -> Option<&'a str> {
441 for header in headers {
442 if header.name.eq_ignore_ascii_case("content-encoding")
443 && let Ok(value) = std::str::from_utf8(header.value)
444 {
445 return Some(value.trim());
446 }
447 }
448 None
449 }
450
451 pub fn get_content_type<'a>(headers: &'a [Header<'_>]) -> Option<&'a str> {
452 for header in headers {
453 if header.name.eq_ignore_ascii_case("content-type")
454 && let Ok(value) = str::from_utf8(header.value)
455 {
456 return Some(value.trim());
457 }
458 }
459 None
460 }
461
462 pub async fn process_response_body(body: &[u8], headers: &[Header<'_>]) -> Result<Vec<u8>> {
463 let is_chunked = RequestParser::is_chunked_transfer(headers);
464 let dechunked_body = if is_chunked {
465 debug!(
466 "Processing chunked response body, size before dechunking: {}",
467 body.len()
468 );
469 let result = RequestParser::process_chunked_body(body);
470 debug!(
471 "Dechunked response body size: {} -> {}",
472 body.len(),
473 result.len()
474 );
475 result
476 } else {
477 body.to_vec()
478 };
479
480 let content_encoding = Self::get_content_encoding(headers);
481
482 if let Some(encoding) = content_encoding {
483 debug!("Detected content encoding: {}", encoding);
484 }
485
486 match content_encoding {
487 Some(encoding) if encoding == "gzip" || encoding.eq_ignore_ascii_case("gzip") => {
488 debug!(
489 "Decompressing gzip encoded content, size: {}",
490 dechunked_body.len()
491 );
492
493 let mut decompressed = None;
494
495 match Self::decompress_gzip(&dechunked_body).await {
496 Ok(result) if !result.is_empty() && result.len() > dechunked_body.len() / 2 => {
497 debug!(
498 "Successfully decompressed gzip content: {} bytes -> {} bytes",
499 dechunked_body.len(),
500 result.len()
501 );
502 decompressed = Some(result);
503 }
504 Ok(result) if !result.is_empty() => {
505 debug!(
506 "Partial gzip decompression: {} bytes -> {} bytes, trying alternative approaches",
507 dechunked_body.len(),
508 result.len()
509 );
510 decompressed = Some(result);
511 }
512 _ => {
513 debug!("Standard gzip decompression failed, trying alternative approaches");
514 }
515 }
516
517 if (decompressed.is_none() || decompressed.as_ref().unwrap().is_empty())
518 && let Some(gzip_start) = Self::find_gzip_header(&dechunked_body)
519 && gzip_start > 0
520 {
521 debug!(
522 "Found gzip header at offset {}, attempting alternative decompression",
523 gzip_start
524 );
525 match Self::decompress_gzip(&dechunked_body[gzip_start..]).await {
526 Ok(result) if !result.is_empty() => {
527 debug!(
528 "Alternative gzip decompression succeeded: {} bytes",
529 result.len()
530 );
531 decompressed = Some(result);
532 }
533 _ => {
534 debug!(
535 "Alternative gzip decompression failed or produced empty output"
536 );
537 }
538 }
539 }
540
541 match decompressed {
542 Some(result) if !result.is_empty() => {
543 debug!("Final decompressed size: {} bytes", result.len());
544 Ok(result)
545 }
546 _ => {
547 debug!(
548 "All gzip decompression attempts failed, returning original content"
549 );
550 Ok(dechunked_body)
551 }
552 }
553 }
554 Some(encoding) if encoding == "br" || encoding.eq_ignore_ascii_case("br") => {
555 debug!(
556 "Decompressing brotli encoded content, size: {}",
557 dechunked_body.len()
558 );
559 match Self::decompress_brotli(&dechunked_body).await {
560 Ok(decompressed) => {
561 debug!(
562 "Successfully decompressed brotli content: {} bytes -> {} bytes",
563 dechunked_body.len(),
564 decompressed.len()
565 );
566 Ok(decompressed)
567 }
568 Err(e) => {
569 debug!(
570 "Brotli decompression failed: {:?}, returning original content",
571 e
572 );
573 Ok(dechunked_body)
574 }
575 }
576 }
577 Some(encoding) => {
578 debug!("Unknown content encoding: {}, returning as-is", encoding);
579 Ok(dechunked_body)
580 }
581 None => Ok(dechunked_body),
582 }
583 }
584
585 fn find_gzip_header(data: &[u8]) -> Option<usize> {
586 data.windows(2)
587 .position(|window| window[0] == 0x1f && window[1] == 0x8b)
588 }
589
590 pub async fn decompress_gzip(data: &[u8]) -> Result<Vec<u8>> {
591 let data = data.to_vec();
592
593 if data.len() < 8 {
594 return Ok(data);
595 }
596
597 if data.len() >= 2 && data[0] == 0x1F && data[1] == 0x8B {
598 task::spawn_blocking(move || {
599 let mut decoder = GzipMembers::new(&data[..]);
600 let mut decompressed_data = Vec::new();
601 if decoder.read_to_end(&mut decompressed_data).is_err() {
602 if decompressed_data.is_empty() {
603 return Ok(data);
604 } else {
605 return Ok(decompressed_data);
606 }
607 }
608 Ok(decompressed_data)
609 })
610 .await?
611 } else {
612 Ok(data)
613 }
614 }
615
616 pub async fn decompress_brotli(data: &[u8]) -> Result<Vec<u8>> {
617 if data.is_empty() || data.len() < 4 {
618 return Ok(data.to_vec());
619 }
620
621 let data = data.to_vec();
622 task::spawn_blocking(move || {
623 let mut reader = Decompressor::new(&data[..], data.len());
624 let mut decompressed_data = Vec::new();
625
626 match reader.read_to_end(&mut decompressed_data) {
627 Ok(_) => {
628 if !decompressed_data.is_empty() {
629 debug!(
630 "Successfully decompressed brotli data: {} -> {} bytes",
631 data.len(),
632 decompressed_data.len()
633 );
634 Ok(decompressed_data)
635 } else {
636 debug!("Brotli decompression produced empty result, using original");
637 Ok(data)
638 }
639 }
640 Err(e) => {
641 debug!("Brotli decompression error: {:?}", e);
642
643 let mut reader = Decompressor::new(&data[..], 65536);
644 let mut decompressed_data = Vec::new();
645
646 match reader.read_to_end(&mut decompressed_data) {
647 Ok(_) if !decompressed_data.is_empty() => {
648 debug!(
649 "Alternative brotli decompression succeeded: {} bytes",
650 decompressed_data.len()
651 );
652 Ok(decompressed_data)
653 }
654 _ => {
655 debug!("All brotli decompression attempts failed, returning original");
656 Ok(data)
657 }
658 }
659 }
660 }
661 })
662 .await?
663 }
664
665 fn bytes_to_utf8_string(bytes: &[u8]) -> String {
666 if let Ok(s) = std::str::from_utf8(bytes) {
667 return s.to_string();
668 }
669
670 String::from_utf8_lossy(bytes).to_string()
671 }
672
673 fn identify_content_category(content_type: Option<&str>, body: &[u8]) -> ContentCategory {
674 if body.is_empty() {
675 return ContentCategory::Empty;
676 }
677
678 if let Some(ct) = content_type {
679 let ct_lower = ct.to_lowercase();
680
681 if ct_lower.contains("svg") {
682 return ContentCategory::Svg;
683 }
684
685 if ct_lower.starts_with("image/") {
686 return ContentCategory::Binary;
687 }
688 if ct_lower.starts_with("audio/") {
689 return ContentCategory::Binary;
690 }
691 if ct_lower.starts_with("video/") {
692 return ContentCategory::Binary;
693 }
694 if ct_lower.contains("font/") || ct_lower.contains("application/font") {
695 return ContentCategory::Font;
696 }
697 if ct_lower.contains("application/pdf")
698 || ct_lower.contains("application/msword")
699 || ct_lower.contains("octet-stream")
700 {
701 return ContentCategory::Binary;
702 }
703
704 if ct_lower.contains("json") {
705 return ContentCategory::Json;
706 }
707 if ct_lower.contains("javascript") {
708 return ContentCategory::JavaScript;
709 }
710 if ct_lower.contains("css") {
711 return ContentCategory::Css;
712 }
713 if ct_lower.contains("html") {
714 return ContentCategory::Html;
715 }
716 if ct_lower.contains("xml") {
717 return ContentCategory::Xml;
718 }
719 if ct_lower.starts_with("text/") {
720 return ContentCategory::Text;
721 }
722 }
723
724 if body.len() > 2 {
725 if body.windows(4).any(|w| w == b"<svg") {
726 return ContentCategory::Svg;
727 }
728
729 if (body[0] == b'{' && body.contains(&b'}'))
730 || (body[0] == b'[' && body.contains(&b']'))
731 {
732 return ContentCategory::Json;
733 }
734
735 if body.len() > 5
736 && (body.starts_with(b"<?xml")
737 || body.starts_with(b"<!DOC")
738 || body.windows(5).any(|w| w == b"<html")
739 || body.windows(5).any(|w| w == b"<body")
740 || body.windows(5).any(|w| w == b"<head"))
741 {
742 if body.windows(5).any(|w| w == b"<html") || body.windows(5).any(|w| w == b"<body")
743 {
744 return ContentCategory::Html;
745 }
746
747 return ContentCategory::Xml;
748 }
749
750 if body.starts_with(&[0xFF, 0xD8])
751 || body.starts_with(&[0x89, 0x50, 0x4E, 0x47])
752 || body.starts_with(&[0x47, 0x49, 0x46])
753 {
754 return ContentCategory::Binary;
755 }
756
757 let likely_binary = body
758 .iter()
759 .take(32)
760 .filter(|&&b| b < 9 && b != b'\t' && b != b'\n' && b != b'\r')
761 .count()
762 > 8;
763 if likely_binary {
764 return ContentCategory::Binary;
765 }
766 }
767
768 ContentCategory::Text
769 }
770
771 pub async fn format_body_async(body: &[u8], headers: &[Header<'_>]) -> Result<String> {
772 debug!(
773 "Processing response body for formatting, size: {} bytes",
774 body.len()
775 );
776
777 let processed_body = match Self::process_response_body(body, headers).await {
778 Ok(processed) => {
779 debug!(
780 "Successfully processed body: {} bytes -> {} bytes",
781 body.len(),
782 processed.len()
783 );
784 processed
785 }
786 Err(e) => {
787 debug!("Failed to process body: {:?}, using original", e);
788 body.to_vec()
789 }
790 };
791
792 let content_type = Self::get_content_type(headers);
793 if let Some(ct) = content_type {
794 debug!("Content-Type: {}", ct);
795 } else {
796 debug!("No Content-Type header found");
797 }
798
799 match Self::format_body(&processed_body, content_type) {
800 Ok(formatted) => {
801 debug!("Successfully formatted body: {} bytes", formatted.len());
802 Ok(formatted)
803 }
804 Err(e) => {
805 debug!(
806 "Failed to format body: {:?}, trying direct string conversion",
807 e
808 );
809
810 if let Ok(text) = std::str::from_utf8(&processed_body) {
811 debug!("Direct string conversion succeeded");
812 Ok(text.to_string())
813 } else {
814 debug!("Direct string conversion failed, using lossy conversion");
815 Ok(String::from_utf8_lossy(&processed_body).to_string())
816 }
817 }
818 }
819 }
820
821 pub fn format_body(body: &[u8], content_type: Option<&str>) -> Result<String> {
822 let category = Self::identify_content_category(content_type, body);
823 debug!("Content category identified as: {:?}", category);
824
825 let result = match category {
826 ContentCategory::Empty => "# <empty body>".to_string(),
827
828 ContentCategory::Binary => {
829 let content_desc = content_type.unwrap_or("binary");
830 let preview = if !body.is_empty() {
831 let preview_size = std::cmp::min(body.len(), 64);
832 let bytes: Vec<String> = body[..preview_size]
833 .iter()
834 .map(|b| format!("{b:02x}"))
835 .collect();
836 format!("\n# Preview: {} ", bytes.join(" "))
837 } else {
838 "".to_string()
839 };
840 format!(
841 "# <binary data: {} format, {} bytes>{}",
842 content_desc,
843 body.len(),
844 preview
845 )
846 }
847
848 ContentCategory::Font => {
849 let font_type = content_type.unwrap_or("font");
850 format!(
851 "# <binary data: {} format, {} bytes>",
852 font_type,
853 body.len()
854 )
855 }
856
857 ContentCategory::Json => {
858 let body_str = Self::bytes_to_utf8_string(body);
859 let trimmed = body_str.trim();
860 debug!(
861 "Attempting to format JSON content, size: {} bytes",
862 trimmed.len()
863 );
864
865 match serde_json::from_str::<serde_json::Value>(trimmed) {
866 Ok(json_value) => {
867 debug!("Successfully parsed JSON, pretty-printing");
868 match serde_json::to_string_pretty(&json_value) {
869 Ok(pretty_json) => {
870 debug!(
871 "Successfully pretty-printed JSON: {} bytes",
872 pretty_json.len()
873 );
874 pretty_json
875 }
876 Err(e) => {
877 debug!("Error pretty-printing JSON: {:?}", e);
878 format!("# <JSON content - error formatting>\n{trimmed}")
879 }
880 }
881 }
882 Err(e) => {
883 debug!("Error parsing JSON: {:?}", e);
884
885 if trimmed.contains("}\n{") || trimmed.contains("}\r\n{") {
886 debug!("Detected potential JSON stream, formatting line by line");
887 let mut formatted = String::new();
888 let mut line_count = 0;
889
890 for line in trimmed.lines() {
891 let line = line.trim();
892 if line.is_empty() {
893 continue;
894 }
895
896 line_count += 1;
897 if line_count > 100 {
898 formatted.push_str("\n# ... more JSON objects omitted ...");
899 break;
900 }
901
902 match serde_json::from_str::<serde_json::Value>(line) {
903 Ok(json_value) => {
904 if let Ok(pretty) =
905 serde_json::to_string_pretty(&json_value)
906 {
907 formatted.push_str(&pretty);
908 } else {
909 formatted.push_str(line);
910 }
911 }
912 Err(_) => {
913 formatted.push_str(line);
914 }
915 }
916 formatted.push_str("\n\n");
917 }
918
919 if !formatted.is_empty() {
920 debug!("Formatted JSON stream: {} lines", line_count);
921 formatted
922 } else {
923 format!("# <JSON content - invalid JSON>\n{trimmed}")
924 }
925 } else {
926 format!("# <JSON content - invalid JSON>\n{trimmed}")
927 }
928 }
929 }
930 }
931
932 ContentCategory::JavaScript => {
933 let js_str = Self::bytes_to_utf8_string(body);
934 debug!(
935 "Formatting JavaScript content, size: {} bytes",
936 js_str.len()
937 );
938
939 let mut formatted = String::new();
940 let mut indent_level: usize = 0;
941 let mut in_string = false;
942 let mut prev_char = '\0';
943
944 for c in js_str.chars() {
945 if (c == '"' || c == '\'') && prev_char != '\\' {
946 in_string = !in_string;
947 }
948
949 if !in_string {
950 if c == '{' || c == '[' {
951 formatted.push(c);
952 formatted.push('\n');
953 indent_level += 1;
954 for _ in 0..indent_level {
955 formatted.push_str(" ");
956 }
957 continue;
958 } else if c == '}' || c == ']' {
959 formatted.push('\n');
960 indent_level = indent_level.saturating_sub(1);
961 for _ in 0..indent_level {
962 formatted.push_str(" ");
963 }
964 formatted.push(c);
965 continue;
966 } else if c == ';' {
967 formatted.push(c);
968 formatted.push('\n');
969 for _ in 0..indent_level {
970 formatted.push_str(" ");
971 }
972 continue;
973 }
974 }
975
976 formatted.push(c);
977 prev_char = c;
978 }
979
980 if formatted.len() > js_str.len() * 2 || formatted.len() < js_str.len() / 2 {
981 debug!("JavaScript formatting produced unusual result, using original");
982 js_str
983 } else {
984 debug!(
985 "Successfully formatted JavaScript: {} bytes",
986 formatted.len()
987 );
988 formatted
989 }
990 }
991
992 ContentCategory::Css => {
993 let css_str = Self::bytes_to_utf8_string(body);
994 format!("# <CSS content>\n{}", css_str.trim())
995 }
996
997 ContentCategory::Html => {
998 let html_str = Self::bytes_to_utf8_string(body);
999 format!(
1000 "# <HTML content ({} bytes)>\n{}",
1001 body.len(),
1002 html_str.trim()
1003 )
1004 }
1005
1006 ContentCategory::Xml => {
1007 let xml_str = Self::bytes_to_utf8_string(body);
1008 format!("# <XML content>\n{}", xml_str.trim())
1009 }
1010
1011 ContentCategory::Svg => {
1012 let svg_str = Self::bytes_to_utf8_string(body);
1013 format!("# <SVG content>\n{}", svg_str.trim())
1014 }
1015
1016 ContentCategory::Text => {
1017 let text_str = Self::bytes_to_utf8_string(body);
1018
1019 let type_info = if let Some(ct) = content_type {
1020 format!(" of type {ct}")
1021 } else {
1022 "".to_string()
1023 };
1024
1025 if content_type.is_some()
1026 && !content_type.unwrap().to_lowercase().starts_with("text/")
1027 {
1028 format!("# <Unknown content{}>\n{}", type_info, text_str.trim())
1029 } else {
1030 text_str.trim().to_string()
1031 }
1032 }
1033 };
1034
1035 Ok(result)
1036 }
1037
1038 pub fn is_content_too_large(content_length: usize) -> bool {
1039 content_length > 100 * 1024 * 1024
1040 }
1041}
1042
1043#[cfg(test)]
1044mod tests {
1045 use super::*;
1046
1047 #[test]
1048 fn test_request_parser() {
1049 let request =
1050 b"GET /api/users HTTP/1.1\r\nHost: example.com\r\nUser-Agent: test\r\n\r\nHello";
1051 let (method, path, version, headers) = RequestParser::parse(request).unwrap();
1052
1053 assert_eq!(method, Some("GET"));
1054 assert_eq!(path, Some("/api/users"));
1055 assert_eq!(version, Some(1));
1056 assert_eq!(headers.len(), 2);
1057 assert_eq!(headers[0].name, "Host");
1058 assert_eq!(
1059 std::str::from_utf8(headers[0].value).unwrap(),
1060 "example.com"
1061 );
1062 }
1063
1064 #[test]
1065 fn test_request_parser_partial() {
1066 let partial_request = b"GET /api/";
1067 let (method, path, version, headers) = RequestParser::parse(partial_request).unwrap();
1068
1069 assert_eq!(method, None);
1070 assert_eq!(path, None);
1071 assert_eq!(version, None);
1072 assert!(headers.is_empty());
1073 }
1074
1075 #[test]
1076 fn test_get_content_length() {
1077 let mut headers = [EMPTY_HEADER; 2];
1078 headers[0].name = "Content-Length";
1079 headers[0].value = b"1024";
1080 headers[1].name = "Host";
1081 headers[1].value = b"example.com";
1082
1083 let length = RequestParser::get_content_length(&headers);
1084 assert_eq!(length, 1024);
1085
1086 let empty_headers: [Header; 0] = [];
1087 assert_eq!(RequestParser::get_content_length(&empty_headers), 0);
1088
1089 headers[0].value = b"invalid";
1090 assert_eq!(RequestParser::get_content_length(&headers), 0);
1091 }
1092
1093 #[test]
1094 fn test_get_content_encoding() {
1095 let mut headers = [EMPTY_HEADER; 2];
1096 headers[0].name = "Content-Encoding";
1097 headers[0].value = b"gzip";
1098 headers[1].name = "Host";
1099 headers[1].value = b"example.com";
1100
1101 let encoding = RequestParser::get_content_encoding(&headers);
1102 assert_eq!(encoding, Some("gzip"));
1103
1104 headers[0].value = b"br";
1105 assert_eq!(RequestParser::get_content_encoding(&headers), Some("br"));
1106
1107 let empty_headers: [Header; 0] = [];
1108 assert_eq!(RequestParser::get_content_encoding(&empty_headers), None);
1109 }
1110
1111 #[test]
1112 fn test_is_gzip_encoded() {
1113 let mut headers = [EMPTY_HEADER; 1];
1114 headers[0].name = "Content-Encoding";
1115 headers[0].value = b"gzip";
1116
1117 assert!(RequestParser::is_gzip_encoded(&headers));
1118
1119 headers[0].value = b"br";
1120 assert!(!RequestParser::is_gzip_encoded(&headers));
1121
1122 let empty_headers: [Header; 0] = [];
1123 assert!(!RequestParser::is_gzip_encoded(&empty_headers));
1124 }
1125
1126 #[test]
1127 fn test_is_brotli_encoded() {
1128 let mut headers = [EMPTY_HEADER; 1];
1129 headers[0].name = "Content-Encoding";
1130 headers[0].value = b"br";
1131
1132 assert!(RequestParser::is_brotli_encoded(&headers));
1133
1134 headers[0].value = b"gzip";
1135 assert!(!RequestParser::is_brotli_encoded(&headers));
1136
1137 let empty_headers: [Header; 0] = [];
1138 assert!(!RequestParser::is_brotli_encoded(&empty_headers));
1139 }
1140
1141 #[test]
1142 fn test_is_chunked_transfer() {
1143 let mut headers = [EMPTY_HEADER; 1];
1144 headers[0].name = "Transfer-Encoding";
1145 headers[0].value = b"chunked";
1146
1147 assert!(RequestParser::is_chunked_transfer(&headers));
1148
1149 headers[0].value = b"compress";
1150 assert!(!RequestParser::is_chunked_transfer(&headers));
1151
1152 let empty_headers: [Header; 0] = [];
1153 assert!(!RequestParser::is_chunked_transfer(&empty_headers));
1154 }
1155
1156 #[test]
1157 fn test_extract_body() {
1158 let request = b"GET /api/users HTTP/1.1\r\nHost: example.com\r\n\r\nHello World";
1159 let body = RequestParser::extract_body(request);
1160 assert_eq!(body, Some(b"Hello World" as &[u8]));
1161
1162 let no_body = b"GET /api/users HTTP/1.1\r\nHost: example.com";
1163 assert_eq!(RequestParser::extract_body(no_body), None);
1164 }
1165
1166 #[test]
1167 fn test_process_chunked_body() {
1168 let chunked_body = b"7\r\nHello, \r\n5\r\nWorld\r\n0\r\n\r\n";
1169 let processed = RequestParser::process_chunked_body(chunked_body);
1170 assert_eq!(processed, b"Hello, World");
1171
1172 let empty_body = b"";
1173 let processed = RequestParser::process_chunked_body(empty_body);
1174 assert!(processed.is_empty());
1175
1176 let incomplete = b"7\r\nHello,";
1177 let processed = RequestParser::process_chunked_body(incomplete);
1178 assert!(processed.len() >= 6);
1179 assert!(processed.starts_with(b"Hello,"));
1180
1181 let invalid = b"XYZ\r\nHello";
1182 let processed = RequestParser::process_chunked_body(invalid);
1183 assert!(processed.len() >= 5);
1184 assert!(processed.ends_with(b"Hello"));
1185
1186 let complex = b"5\r\nHello\r\n5\r\nWorld\r\n0\r\n\r\nExtra";
1187 let processed = RequestParser::process_chunked_body(complex);
1188 assert_eq!(processed, b"HelloWorld");
1189 }
1190
1191 #[test]
1192 fn test_parse_chunk() {
1193 let mut chunks_found = 0;
1194
1195 let data = b"5\r\nHello\r\n";
1196 let result = RequestParser::parse_chunk(data, &mut chunks_found).unwrap();
1197 match result {
1198 ChunkParseResult::Complete {
1199 chunk_data,
1200 next_pos,
1201 } => {
1202 assert_eq!(chunk_data, b"Hello");
1203 assert_eq!(next_pos, data.len());
1204 assert_eq!(chunks_found, 1);
1205 }
1206 _ => panic!("Expected Complete result"),
1207 }
1208
1209 chunks_found = 0;
1210 let data = b"0\r\n\r\n";
1211 let result = RequestParser::parse_chunk(data, &mut chunks_found).unwrap();
1212 match result {
1213 ChunkParseResult::EndOfChunks { next_pos } => {
1214 assert_eq!(next_pos, 5);
1215 assert_eq!(chunks_found, 1);
1216 }
1217 _ => panic!("Expected EndOfChunks result"),
1218 }
1219
1220 chunks_found = 0;
1221 let data = b"5\r\nHel";
1222 let result = RequestParser::parse_chunk(data, &mut chunks_found).unwrap();
1223 match result {
1224 ChunkParseResult::Incomplete {
1225 chunk_data,
1226 consumed,
1227 } => {
1228 assert_eq!(chunk_data, Some(b"Hel" as &[u8]));
1229 assert_eq!(consumed, data.len());
1230 assert_eq!(chunks_found, 1);
1231 }
1232 _ => panic!("Expected Incomplete result"),
1233 }
1234
1235 chunks_found = 0;
1236 let data = b"\r\nignore";
1237 let result = RequestParser::parse_chunk(data, &mut chunks_found).unwrap();
1238 match result {
1239 ChunkParseResult::Skip { consumed } => {
1240 assert_eq!(consumed, 2);
1241 assert_eq!(chunks_found, 0);
1242 }
1243 _ => panic!("Expected Skip result"),
1244 }
1245
1246 chunks_found = 0;
1247 let data = b"A;extension=value\r\n1234567890\r\n";
1248 let result = RequestParser::parse_chunk(data, &mut chunks_found).unwrap();
1249 match result {
1250 ChunkParseResult::Complete {
1251 chunk_data,
1252 next_pos,
1253 } => {
1254 assert_eq!(chunk_data, b"1234567890");
1255 assert_eq!(next_pos, data.len());
1256 assert_eq!(chunks_found, 1);
1257 }
1258 _ => panic!("Expected Complete result"),
1259 }
1260 }
1261
1262 #[test]
1263 fn test_handle_chunked_edge_cases() {
1264 let body = b"";
1265 let result = Vec::new();
1266 let output = RequestParser::handle_chunked_edge_cases(body, result, 0, false);
1267 assert!(output.is_empty());
1268
1269 let body = b"Hello World";
1270 let result = Vec::new();
1271 let output = RequestParser::handle_chunked_edge_cases(body, result, 0, false);
1272 assert_eq!(output, body);
1273
1274 let body = b"5\r\nHello\r\n3\r\nWor";
1275 let mut result = Vec::new();
1276 result.extend_from_slice(b"Hello");
1277 let output = RequestParser::handle_chunked_edge_cases(body, result, 1, true);
1278 assert!(output.contains(&b'H'));
1279 assert!(output.contains(&b'e'));
1280 assert!(output.contains(&b'l'));
1281 assert!(output.contains(&b'o'));
1282 assert!(output.contains(&b'W'));
1283 assert!(output.contains(&b'o'));
1284 assert!(output.contains(&b'r'));
1285 }
1286
1287 #[test]
1288 fn test_response_parser() {
1289 let response =
1290 b"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 2\r\n\r\n{}";
1291 let (status, headers) = ResponseParser::parse(response).unwrap();
1292
1293 assert_eq!(status, Some(200));
1294 assert_eq!(headers.len(), 2);
1295 assert_eq!(headers[0].name, "Content-Type");
1296 assert_eq!(
1297 std::str::from_utf8(headers[0].value).unwrap(),
1298 "application/json"
1299 );
1300 }
1301
1302 #[test]
1303 fn test_response_parser_partial() {
1304 let partial_response = b"HTTP/1.1 20";
1305 let (status, headers) = ResponseParser::parse(partial_response).unwrap();
1306
1307 assert_eq!(status, None);
1308 assert!(headers.is_empty());
1309 }
1310
1311 #[test]
1312 fn test_is_image() {
1313 let mut headers = [EMPTY_HEADER; 1];
1314 headers[0].name = "Content-Type";
1315 headers[0].value = b"image/jpeg";
1316
1317 assert!(BodyParser::is_image(&headers));
1318
1319 headers[0].value = b"text/plain";
1320 assert!(!BodyParser::is_image(&headers));
1321 }
1322
1323 #[test]
1324 fn test_get_content_type() {
1325 let mut headers = [EMPTY_HEADER; 1];
1326 headers[0].name = "Content-Type";
1327 headers[0].value = b"application/json";
1328
1329 let content_type = BodyParser::get_content_type(&headers);
1330 assert_eq!(content_type, Some("application/json"));
1331
1332 headers[0].value = b"text/html; charset=utf-8";
1333 let content_type = BodyParser::get_content_type(&headers);
1334 assert_eq!(content_type, Some("text/html; charset=utf-8"));
1335
1336 let empty_headers: [Header; 0] = [];
1337 assert_eq!(BodyParser::get_content_type(&empty_headers), None);
1338 }
1339
1340 #[tokio::test]
1341 async fn test_gzip_decompression() {
1342 let short_data = &[0x1F];
1343 let result = BodyParser::decompress_gzip(short_data).await.unwrap();
1344 assert_eq!(result, short_data);
1345
1346 let non_gzip_data = b"Hello World";
1347 let result = BodyParser::decompress_gzip(non_gzip_data).await.unwrap();
1348 assert_eq!(result, non_gzip_data);
1349
1350 let data_with_gzip_header = b"prefix\x1F\x8Bgzipped";
1351 let index = BodyParser::find_gzip_header(data_with_gzip_header);
1352 assert_eq!(index, Some(6));
1353 }
1354
1355 #[tokio::test]
1356 async fn test_multi_member_gzip_response_keeps_every_member() {
1357 let mut body = Vec::new();
1358 for line in ["first-member-line\n", "second-member-line\n"] {
1359 let mut gzip =
1360 flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
1361 std::io::Write::write_all(&mut gzip, line.as_bytes()).unwrap();
1362 body.extend(gzip.finish().unwrap());
1363 }
1364 let mut headers = [EMPTY_HEADER; 1];
1365 headers[0].name = "Content-Encoding";
1366 headers[0].value = b"gzip";
1367
1368 let result = BodyParser::process_response_body(&body, &headers)
1369 .await
1370 .unwrap();
1371
1372 assert_eq!(result, b"first-member-line\nsecond-member-line\n");
1373 }
1374
1375 #[tokio::test]
1376 async fn test_brotli_decompression() {
1377 let empty_data = &[];
1378 let result = BodyParser::decompress_brotli(empty_data).await.unwrap();
1379 assert_eq!(result, empty_data);
1380
1381 let short_data = &[0x01, 0x02, 0x03];
1382 let result = BodyParser::decompress_brotli(short_data).await.unwrap();
1383 assert_eq!(result, short_data);
1384
1385 let non_br_data = b"Hello World";
1386 let result = BodyParser::decompress_brotli(non_br_data).await.unwrap();
1387 assert_eq!(result, non_br_data);
1388 }
1389
1390 #[test]
1391 fn test_bytes_to_utf8_string() {
1392 let valid_utf8 = b"Hello World";
1393 let result = BodyParser::bytes_to_utf8_string(valid_utf8);
1394 assert_eq!(result, "Hello World");
1395
1396 let invalid_utf8 = &[0xFF, 0xFE, 0xFD];
1397 let result = BodyParser::bytes_to_utf8_string(invalid_utf8);
1398 assert!(!result.is_empty());
1399 }
1400
1401 #[test]
1402 fn test_identify_content_category() {
1403 assert_eq!(
1405 BodyParser::identify_content_category(None, b""),
1406 ContentCategory::Empty
1407 );
1408
1409 assert_eq!(
1411 BodyParser::identify_content_category(Some("image/png"), b"data"),
1412 ContentCategory::Binary
1413 );
1414
1415 assert_eq!(
1416 BodyParser::identify_content_category(Some("application/json"), b"data"),
1417 ContentCategory::Json
1418 );
1419
1420 assert_eq!(
1421 BodyParser::identify_content_category(Some("text/javascript"), b"data"),
1422 ContentCategory::JavaScript
1423 );
1424
1425 assert_eq!(
1426 BodyParser::identify_content_category(Some("text/css"), b"data"),
1427 ContentCategory::Css
1428 );
1429
1430 assert_eq!(
1431 BodyParser::identify_content_category(Some("text/html"), b"data"),
1432 ContentCategory::Html
1433 );
1434
1435 assert_eq!(
1436 BodyParser::identify_content_category(Some("text/xml"), b"data"),
1437 ContentCategory::Xml
1438 );
1439
1440 assert_eq!(
1441 BodyParser::identify_content_category(Some("image/svg+xml"), b"data"),
1442 ContentCategory::Svg
1443 );
1444
1445 assert_eq!(
1446 BodyParser::identify_content_category(Some("text/plain"), b"data"),
1447 ContentCategory::Text
1448 );
1449
1450 assert_eq!(
1451 BodyParser::identify_content_category(Some("font/ttf"), b"data"),
1452 ContentCategory::Font
1453 );
1454
1455 assert_eq!(
1457 BodyParser::identify_content_category(None, b"{\"key\":\"value\"}"),
1458 ContentCategory::Json
1459 );
1460
1461 assert_eq!(
1462 BodyParser::identify_content_category(None, b"<html><body>Hello</body></html>"),
1463 ContentCategory::Html
1464 );
1465
1466 assert_eq!(
1467 BodyParser::identify_content_category(None, b"<svg width=\"100\"></svg>"),
1468 ContentCategory::Svg
1469 );
1470
1471 assert_eq!(
1473 BodyParser::identify_content_category(None, &[0xFF, 0xD8, 0xFF]),
1474 ContentCategory::Binary
1475 );
1476
1477 assert_eq!(
1478 BodyParser::identify_content_category(None, &[0x89, 0x50, 0x4E, 0x47]),
1479 ContentCategory::Binary
1480 );
1481
1482 assert_eq!(
1483 BodyParser::identify_content_category(None, &[0x47, 0x49, 0x46]),
1484 ContentCategory::Binary
1485 );
1486 }
1487
1488 #[tokio::test]
1489 async fn test_process_response_body() {
1490 let mut headers = [EMPTY_HEADER; 1];
1491 headers[0].name = "Transfer-Encoding";
1492 headers[0].value = b"chunked";
1493
1494 let chunked_body = b"5\r\nHello\r\n6\r\n World\r\n0\r\n\r\n";
1495 let result = BodyParser::process_response_body(chunked_body, &headers)
1496 .await
1497 .unwrap();
1498 assert_eq!(result, b"Hello World");
1499
1500 let headers: [Header; 0] = [];
1501 let body = b"Hello World";
1502 let result = BodyParser::process_response_body(body, &headers)
1503 .await
1504 .unwrap();
1505 assert_eq!(result, body);
1506 }
1507
1508 #[tokio::test]
1509 async fn test_format_body() {
1510 let json_body = br#"{"name":"test","value":123}"#;
1511 let formatted = BodyParser::format_body_async(json_body, &[]).await.unwrap();
1512 assert!(formatted.contains("{\n"));
1513
1514 let text_body = b"Hello world";
1515 let formatted = BodyParser::format_body_async(text_body, &[]).await.unwrap();
1516 assert_eq!(formatted, "Hello world");
1517
1518 let empty_body = b"";
1519 let formatted = BodyParser::format_body_async(empty_body, &[])
1520 .await
1521 .unwrap();
1522 assert_eq!(formatted, "# <empty body>");
1523
1524 let binary_data = &[0xFF, 0xD8, 0xFF, 0xE0];
1525 let formatted = BodyParser::format_body_async(binary_data, &[])
1526 .await
1527 .unwrap();
1528 assert!(
1529 formatted.contains("binary"),
1530 "Expected binary format message, got: {formatted}"
1531 );
1532
1533 let jpeg_binary_data = &[0xFF, 0xD8, 0xFF, 0xE0];
1534 let mut headers = [EMPTY_HEADER; 1];
1535 headers[0].name = "Content-Type";
1536 headers[0].value = b"image/jpeg";
1537
1538 let formatted = BodyParser::format_body_async(jpeg_binary_data, &headers)
1539 .await
1540 .unwrap();
1541 assert!(
1542 formatted.contains("image"),
1543 "Expected image format message, got: {formatted}"
1544 );
1545
1546 let css_body = b"body { color: red; }";
1547 let mut headers = [EMPTY_HEADER; 1];
1548 headers[0].name = "Content-Type";
1549 headers[0].value = b"text/css";
1550
1551 let formatted = BodyParser::format_body_async(css_body, &headers)
1552 .await
1553 .unwrap();
1554 assert!(formatted.contains("CSS content"));
1555
1556 let html_body = b"<html><body>Hello</body></html>";
1557 let mut headers = [EMPTY_HEADER; 1];
1558 headers[0].name = "Content-Type";
1559 headers[0].value = b"text/html";
1560
1561 let formatted = BodyParser::format_body_async(html_body, &headers)
1562 .await
1563 .unwrap();
1564 assert!(formatted.contains("HTML content"));
1565
1566 let xml_body = b"<?xml version=\"1.0\"?><root><item>test</item></root>";
1567 let mut headers = [EMPTY_HEADER; 1];
1568 headers[0].name = "Content-Type";
1569 headers[0].value = b"application/xml";
1570
1571 let formatted = BodyParser::format_body_async(xml_body, &headers)
1572 .await
1573 .unwrap();
1574 assert!(formatted.contains("XML content"));
1575
1576 let svg_body = b"<svg width=\"100\" height=\"100\"></svg>";
1577 let mut headers = [EMPTY_HEADER; 1];
1578 headers[0].name = "Content-Type";
1579 headers[0].value = b"image/svg+xml";
1580
1581 let formatted = BodyParser::format_body_async(svg_body, &headers)
1582 .await
1583 .unwrap();
1584 assert!(formatted.contains("SVG content"));
1585
1586 let js_body = b"function test() { return 1; }";
1587 let mut headers = [EMPTY_HEADER; 1];
1588 headers[0].name = "Content-Type";
1589 headers[0].value = b"application/javascript";
1590
1591 let formatted = BodyParser::format_body_async(js_body, &headers)
1592 .await
1593 .unwrap();
1594 assert!(formatted.contains("function"));
1595 }
1596
1597 #[test]
1598 fn test_is_content_too_large() {
1599 assert!(BodyParser::is_content_too_large(101 * 1024 * 1024));
1600
1601 assert!(!BodyParser::is_content_too_large(10 * 1024 * 1024));
1602 }
1603}