reinhardt_core/parsers/
file.rs1use async_trait::async_trait;
2use bytes::Bytes;
3use http::HeaderMap;
4
5use super::parser::{ParseError, ParseResult, ParsedData, Parser, UploadedFile};
6
7#[derive(Debug, Clone)]
9pub struct FileUploadParser {
10 pub max_file_size: Option<usize>,
12 pub field_name: String,
14}
15
16impl FileUploadParser {
17 pub fn get_filename(&self, content_disposition: Option<&str>) -> Result<String, ParseError> {
36 let disposition = content_disposition.ok_or_else(|| {
37 ParseError::ParseError(
38 "Missing filename. Request should include a Content-Disposition header with a filename parameter.".to_string()
39 )
40 })?;
41
42 if disposition.trim().is_empty() {
43 return Err(ParseError::ParseError(
44 "Missing filename. Request should include a Content-Disposition header with a filename parameter.".to_string()
45 ));
46 }
47
48 if let Some(encoded_filename) = Self::extract_encoded_filename(disposition) {
50 return Ok(encoded_filename);
51 }
52
53 if let Some(filename) = Self::extract_standard_filename(disposition) {
55 return Ok(filename);
56 }
57
58 Err(ParseError::ParseError(
59 "Missing filename. Request should include a Content-Disposition header with a filename parameter.".to_string()
60 ))
61 }
62
63 fn extract_encoded_filename(disposition: &str) -> Option<String> {
64 for part in disposition.split(';') {
66 let part = part.trim();
67 if part.starts_with("filename*=") {
68 let value = part.trim_start_matches("filename*=");
69
70 if let Some(first_quote) = value.find('\'') {
73 let rest = &value[first_quote + 1..];
75 if let Some(second_quote) = rest.find('\'') {
76 let encoded = &rest[second_quote + 1..];
77 if let Ok(decoded) = urlencoding::decode(encoded) {
79 return Some(decoded.to_string());
80 }
81 }
82 }
83 }
84 }
85 None
86 }
87
88 fn extract_standard_filename(disposition: &str) -> Option<String> {
89 for part in disposition.split(';') {
90 let part = part.trim();
91 if part.starts_with("filename=") && !part.starts_with("filename*=") {
92 let value = part.trim_start_matches("filename=");
93 let value = value.trim_matches('"').trim_matches('\'');
95 return Some(value.to_string());
96 }
97 }
98 None
99 }
100}
101
102impl FileUploadParser {
103 pub fn new(field_name: impl Into<String>) -> Self {
115 Self {
116 max_file_size: None,
117 field_name: field_name.into(),
118 }
119 }
120 pub fn max_file_size(mut self, size: usize) -> Self {
131 self.max_file_size = Some(size);
132 self
133 }
134}
135
136impl Default for FileUploadParser {
137 fn default() -> Self {
138 Self {
139 max_file_size: None,
140 field_name: "file".to_string(),
141 }
142 }
143}
144
145#[async_trait]
146impl Parser for FileUploadParser {
147 fn media_types(&self) -> Vec<String> {
148 vec!["application/octet-stream".to_string(), "*/*".to_string()]
149 }
150
151 async fn parse(
152 &self,
153 content_type: Option<&str>,
154 body: Bytes,
155 _headers: &HeaderMap,
156 ) -> ParseResult<ParsedData> {
157 let size = body.len();
158
159 if let Some(max_size) = self.max_file_size
161 && size > max_size
162 {
163 return Err(ParseError::ParseError(format!(
164 "File exceeds maximum size of {} bytes",
165 max_size
166 )));
167 }
168
169 let mut file = UploadedFile::new(self.field_name.clone(), body);
170
171 if let Some(ct) = content_type {
172 file = file.with_content_type(ct.to_string());
173 }
174
175 Ok(ParsedData::File(file))
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182
183 #[tokio::test]
184 async fn test_file_upload_parser_valid() {
185 let parser = FileUploadParser::new("upload");
186 let body = Bytes::from("binary file content here");
187 let headers = HeaderMap::new();
188
189 let result = parser
190 .parse(Some("application/octet-stream"), body.clone(), &headers)
191 .await
192 .unwrap();
193
194 match result {
195 ParsedData::File(file) => {
196 assert_eq!(file.name, "upload");
197 assert_eq!(file.data, body);
198 assert_eq!(file.size, body.len());
199 assert_eq!(
200 file.content_type,
201 Some("application/octet-stream".to_string())
202 );
203 }
204 _ => panic!("Expected file data"),
205 }
206 }
207
208 #[tokio::test]
209 async fn test_file_upload_parser_max_size() {
210 let parser = FileUploadParser::new("upload").max_file_size(10);
211 let body = Bytes::from("this is a very long file content that exceeds the limit");
212 let headers = HeaderMap::new();
213
214 let result = parser
215 .parse(Some("application/octet-stream"), body, &headers)
216 .await;
217 assert!(result.is_err());
218 }
219
220 #[tokio::test]
221 async fn test_file_upload_parser_no_content_type() {
222 let parser = FileUploadParser::new("upload");
223 let body = Bytes::from("file content");
224 let headers = HeaderMap::new();
225
226 let result = parser.parse(None, body.clone(), &headers).await.unwrap();
227
228 match result {
229 ParsedData::File(file) => {
230 assert_eq!(file.name, "upload");
231 assert_eq!(file.data, body);
232 assert_eq!(file.content_type, None);
233 }
234 _ => panic!("Expected file data"),
235 }
236 }
237
238 #[test]
239 fn test_file_upload_parser_media_types() {
240 let parser = FileUploadParser::new("upload");
241 let media_types = parser.media_types();
242
243 assert!(media_types.contains(&"application/octet-stream".to_string()));
244 assert!(media_types.contains(&"*/*".to_string()));
245 }
246
247 #[tokio::test]
250 async fn test_file_parse_drf() {
251 let parser = FileUploadParser::new("file");
253 let body = Bytes::from("Test text file");
254 let headers = HeaderMap::new();
255
256 let content_disposition = "Content-Disposition: inline; filename=file.txt";
257 let filename = parser.get_filename(Some(content_disposition)).unwrap();
258
259 let result = parser
260 .parse(Some("application/octet-stream"), body.clone(), &headers)
261 .await
262 .unwrap();
263
264 match result {
265 ParsedData::File(file) => {
266 assert_eq!(file.size, 14);
267 assert_eq!(filename, "file.txt");
268 }
269 _ => panic!("Expected file data"),
270 }
271 }
272
273 #[tokio::test]
274 async fn test_parse_missing_filename() {
275 let parser = FileUploadParser::new("file");
277
278 let result = parser.get_filename(Some(""));
279 assert!(result.is_err());
280 assert_eq!(
281 result.unwrap_err().to_string(),
282 "Parse error: Missing filename. Request should include a Content-Disposition header with a filename parameter."
283 );
284 }
285
286 #[tokio::test]
287 async fn test_parse_missing_filename_none() {
288 let parser = FileUploadParser::new("file");
290
291 let result = parser.get_filename(None);
292 assert!(result.is_err());
293 assert_eq!(
294 result.unwrap_err().to_string(),
295 "Parse error: Missing filename. Request should include a Content-Disposition header with a filename parameter."
296 );
297 }
298
299 #[test]
300 fn test_get_filename() {
301 let parser = FileUploadParser::new("file");
303 let content_disposition = "Content-Disposition: inline; filename=file.txt";
304
305 let filename = parser.get_filename(Some(content_disposition)).unwrap();
306 assert_eq!(filename, "file.txt");
307 }
308
309 #[test]
310 fn test_get_encoded_filename() {
311 let parser = FileUploadParser::new("file");
313
314 let disposition = "inline; filename*=utf-8''%C3%80%C4%A5%C6%A6.txt";
316 let filename = parser.get_filename(Some(disposition)).unwrap();
317 assert_eq!(filename, "ÀĥƦ.txt");
318
319 let disposition = "inline; filename=fallback.txt; filename*=utf-8''%C3%80%C4%A5%C6%A6.txt";
321 let filename = parser.get_filename(Some(disposition)).unwrap();
322 assert_eq!(filename, "ÀĥƦ.txt");
323
324 let disposition =
326 "inline; filename=fallback.txt; filename*=utf-8'en-us'%C3%80%C4%A5%C6%A6.txt";
327 let filename = parser.get_filename(Some(disposition)).unwrap();
328 assert_eq!(filename, "ÀĥƦ.txt");
329 }
330}