shared_framework/utils/
request_parser.rs1use serde::de::DeserializeOwned;
14use std::collections::HashMap;
15
16pub struct RequestParser;
18
19impl RequestParser {
20 pub fn query_param<T: std::str::FromStr>(query: &str, key: &str) -> Option<T> {
23 let params: HashMap<String, String> = serde_urlencoded::from_str(query).unwrap_or_default();
24 params.get(key).and_then(|v| v.parse().ok())
25 }
26
27 pub fn parse_body<T: DeserializeOwned>(body: &[u8]) -> Result<T, serde_json::Error> {
29 serde_json::from_slice(body)
30 }
31
32 pub fn multipart_boundary(content_type: &str) -> Option<String> {
35 content_type.split(';').find_map(|part| {
36 let part = part.trim();
37 let (k, v) = part.split_once('=')?;
38 if k.trim().eq_ignore_ascii_case("boundary") {
39 let b = v.trim().trim_matches('"').trim().to_string();
40 if b.is_empty() || b.len() > 128 { None } else { Some(b) }
41 } else {
42 None
43 }
44 })
45 }
46
47 pub fn parse_multipart(body: &[u8], boundary: &str) -> MultipartBody {
50 MultipartBody::parse(body, boundary)
51 }
52}
53
54#[derive(Debug, Clone)]
56pub struct UploadedFile {
57 pub field_name: String,
59 pub file_name: Option<String>,
61 pub content_type: Option<String>,
63 pub bytes: Vec<u8>,
65}
66
67impl UploadedFile {
68 pub fn len(&self) -> usize {
70 self.bytes.len()
71 }
72
73 pub fn is_empty(&self) -> bool {
75 self.bytes.is_empty()
76 }
77}
78
79#[derive(Debug, Clone, Default)]
81pub struct MultipartBody {
82 pub fields: HashMap<String, Vec<String>>,
84 pub files: Vec<UploadedFile>,
86}
87
88const MAX_PARTS: usize = 512;
91
92impl MultipartBody {
93 pub fn parse(body: &[u8], boundary: &str) -> Self {
96 let mut out = Self::default();
97 if boundary.is_empty() || body.is_empty() {
98 return out;
99 }
100 let delimiter = format!("--{}", boundary);
101 let delim = delimiter.as_bytes();
102 let mut offsets = Vec::new();
107 let mut i = 0;
108 while i + delim.len() <= body.len() {
109 if &body[i..i + delim.len()] == delim
110 && (i == 0 || body[i - 1] == b'\n')
111 && follows_delimiter(&body[i + delim.len()..])
112 {
113 offsets.push(i);
114 i += delim.len();
115 } else {
116 i += 1;
117 }
118 }
119 for (idx, &start) in offsets.iter().enumerate() {
120 if idx >= MAX_PARTS {
121 break;
122 }
123 let mut seg_start = start + delim.len();
124 if body.get(seg_start..seg_start + 2) == Some(b"--".as_slice()) {
126 break;
127 }
128 if body.get(seg_start..seg_start + 2) == Some(b"\r\n".as_slice()) {
130 seg_start += 2;
131 } else if body.get(seg_start..seg_start + 1) == Some(b"\n".as_slice()) {
132 seg_start += 1;
133 }
134 let seg_end = offsets.get(idx + 1).copied().unwrap_or(body.len());
135 if seg_end <= seg_start {
136 continue;
137 }
138 let mut segment = &body[seg_start..seg_end];
139 if segment.ends_with(b"\r\n") {
141 segment = &segment[..segment.len() - 2];
142 } else if segment.ends_with(b"\n") {
143 segment = &segment[..segment.len() - 1];
144 }
145 out.add_part(segment);
146 }
147 out
148 }
149
150 fn add_part(&mut self, segment: &[u8]) {
151 let split = find_subslice(segment, b"\r\n\r\n")
152 .or_else(|| find_subslice(segment, b"\n\n"));
153 let Some(at) = split else { return };
154 let sep_len = if segment[at..].starts_with(b"\r\n\r\n") { 4 } else { 2 };
155 let raw_headers = &segment[..at];
156 let content = &segment[at + sep_len..];
157 let headers = String::from_utf8_lossy(raw_headers);
158
159 let mut name: Option<String> = None;
160 let mut file_name: Option<String> = None;
161 let mut part_content_type: Option<String> = None;
162 for line in headers.split("\r\n") {
163 let line = line.trim();
164 if line.is_empty() {
165 continue;
166 }
167 if let Some((k, v)) = line.split_once(':') {
168 let key = k.trim();
169 let val = v.trim().to_string();
170 if key.eq_ignore_ascii_case("content-disposition") {
171 let (disp_name, disp_file) = parse_disposition(&val);
172 if disp_name.is_some() {
173 name = disp_name;
174 }
175 if disp_file.is_some() {
176 file_name = disp_file;
177 }
178 } else if key.eq_ignore_ascii_case("content-type") {
179 part_content_type = Some(val.split(';').next().unwrap_or("").trim().to_string());
180 }
181 }
182 }
183 let Some(name) = name.filter(|n| !n.is_empty()) else { return };
184 let is_file = match (&file_name, &part_content_type) {
188 (Some(f), _) if !f.is_empty() => true,
189 (_, Some(ct)) if !ct.is_empty() && ct != "text/plain" => true,
190 _ => false,
191 };
192 if is_file {
193 self.files.push(UploadedFile {
194 field_name: name,
195 file_name: file_name.filter(|f| !f.is_empty()),
196 content_type: part_content_type.filter(|c| !c.is_empty()),
197 bytes: content.to_vec(),
198 });
199 } else {
200 self.fields
201 .entry(name)
202 .or_default()
203 .push(String::from_utf8_lossy(content).into_owned());
204 }
205 }
206
207 pub fn field(&self, name: &str) -> Option<&str> {
209 self.fields.get(name).and_then(|v| v.first().map(|s| s.as_str()))
210 }
211}
212
213fn parse_disposition(value: &str) -> (Option<String>, Option<String>) {
215 let mut name = None;
216 let mut filename = None;
217 for part in value.split(';').skip(1) {
218 let part = part.trim();
219 if let Some((k, v)) = part.split_once('=') {
220 let v = v.trim().trim_matches('"').to_string();
221 if k.trim().eq_ignore_ascii_case("name") {
222 name = Some(v);
223 } else if k.trim().eq_ignore_ascii_case("filename") {
224 filename = Some(v);
225 }
226 }
227 }
228 (name, filename)
229}
230
231fn find_subslice(haystack: &[u8], needle: &[u8]) -> Option<usize> {
232 if needle.is_empty() || haystack.len() < needle.len() {
233 return None;
234 }
235 (0..=haystack.len() - needle.len()).find(|&i| &haystack[i..i + needle.len()] == needle)
236}
237
238fn follows_delimiter(rest: &[u8]) -> bool {
240 rest.is_empty()
241 || rest.starts_with(b"--")
242 || rest.starts_with(b"\r\n")
243 || rest.starts_with(b"\n")
244}