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