spikard_core/bindings/
response.rs1use crate::CompressionConfig;
2use brotli::CompressorWriter;
3use flate2::Compression;
4use flate2::write::GzEncoder;
5use std::collections::HashMap;
6use std::io::Write;
7
8#[derive(Clone, Debug)]
10pub struct RawResponse {
11 pub status: u16,
12 pub headers: HashMap<String, String>,
13 pub body: Vec<u8>,
14}
15
16impl RawResponse {
17 #[must_use]
19 #[allow(clippy::missing_const_for_fn)]
20 pub fn new(status: u16, headers: HashMap<String, String>, body: Vec<u8>) -> Self {
21 Self { status, headers, body }
22 }
23
24 pub fn apply_compression(&mut self, request_headers: &HashMap<String, String>, compression: &CompressionConfig) {
26 if self.body.is_empty() || self.status == 206 {
27 return;
28 }
29 if self
30 .headers
31 .keys()
32 .any(|key| key.eq_ignore_ascii_case("content-encoding"))
33 {
34 return;
35 }
36 if self.body.len() < compression.min_size {
37 return;
38 }
39
40 let accept_encoding = header_value(request_headers, "Accept-Encoding").map(str::to_ascii_lowercase);
41 let accepts_brotli = accept_encoding.as_ref().is_some_and(|value| value.contains("br"));
42 if compression.brotli && accepts_brotli && self.try_compress_brotli(compression) {
43 return;
44 }
45
46 let accepts_gzip = accept_encoding.as_ref().is_some_and(|value| value.contains("gzip"));
47 if compression.gzip && accepts_gzip {
48 self.try_compress_gzip(compression);
49 }
50 }
51
52 fn try_compress_brotli(&mut self, compression: &CompressionConfig) -> bool {
53 let quality = compression.quality.min(11);
54 let mut writer = CompressorWriter::new(Vec::new(), 4096, quality, 22);
55 if writer.write_all(&self.body).is_err() || writer.flush().is_err() {
56 return false;
57 }
58 let compressed = writer.into_inner();
59 if compressed.is_empty() {
60 return false;
61 }
62 self.finalize_encoded_body("br", compressed);
63 true
64 }
65
66 fn try_compress_gzip(&mut self, compression: &CompressionConfig) -> bool {
67 let mut encoder = GzEncoder::new(Vec::new(), Compression::new(compression.quality));
68 if encoder.write_all(&self.body).is_err() {
69 return false;
70 }
71 let compressed = encoder.finish().unwrap_or_else(|_| Vec::new());
72 if compressed.is_empty() {
73 return false;
74 }
75 self.finalize_encoded_body("gzip", compressed);
76 true
77 }
78
79 fn finalize_encoded_body(&mut self, encoding: &str, compressed: Vec<u8>) {
80 self.body = compressed;
81 self.headers
82 .insert("content-encoding".to_string(), encoding.to_string());
83 self.headers.insert("vary".to_string(), "Accept-Encoding".to_string());
84 self.headers
85 .insert("content-length".to_string(), self.body.len().to_string());
86 }
87}
88
89fn header_value<'a>(headers: &'a HashMap<String, String>, name: &str) -> Option<&'a str> {
90 headers.iter().find_map(|(key, value)| {
91 if key.eq_ignore_ascii_case(name) {
92 Some(value.as_str())
93 } else {
94 None
95 }
96 })
97}
98
99#[derive(Clone, Debug)]
101pub struct StaticAsset {
102 pub route: String,
103 pub headers: HashMap<String, String>,
104 pub body: Vec<u8>,
105}
106
107impl StaticAsset {
108 #[must_use]
110 pub fn serve(&self, method: &str, normalized_path: &str) -> Option<RawResponse> {
111 if !method.eq_ignore_ascii_case("GET") && !method.eq_ignore_ascii_case("HEAD") {
112 return None;
113 }
114 if self.route != normalized_path {
115 return None;
116 }
117
118 let mut headers = self.headers.clone();
119 headers
120 .entry("content-length".to_string())
121 .or_insert_with(|| self.body.len().to_string());
122 let body = if method.eq_ignore_ascii_case("HEAD") {
123 Vec::new()
124 } else {
125 self.body.clone()
126 };
127
128 Some(RawResponse::new(200, headers, body))
129 }
130}