1use console::style;
2use hyper::HeaderMap;
3use serde_json::{Map, Value};
4use tokio::task;
5
6use std::{collections::HashMap, fs, path::Path};
7
8use crate::{
9 constant::CSV_RECORDS_DEFAULT_KEY,
10 json_path_util::resolve_with_json_compatible_extensions,
11 response::{
12 confine::confine, error_response::not_found_response, json_response::json_response,
13 },
14 response_handler::ResponseHandler,
15 types::BoxBody,
16};
17
18use super::{
19 error_response::internal_server_error_response,
20 text_response::text_response,
21 util::{
22 binary_content_type, file_extension, json_value_with_jsonpath_key, text_file_content_type,
23 },
24};
25
26pub struct FileResponse {
27 file_path: String,
28 csv_records_key: Option<String>,
29 text_content: Option<String>,
30 binary_content: Option<Vec<u8>>,
31 custom_headers: Option<HashMap<String, Option<String>>>,
32 request_headers: HeaderMap,
33 confine_to: Option<std::path::PathBuf>,
38 cors_allow_credentials_origins: Vec<String>,
40}
41
42impl FileResponse {
43 pub fn new(
45 file_path: &str,
46 custom_headers: Option<&HashMap<String, Option<String>>>,
47 request_headers: &HeaderMap,
48 confine_to: Option<&Path>,
49 cors_allow_credentials_origins: &[String],
50 ) -> Self {
51 FileResponse {
52 file_path: file_path.to_owned(),
53 csv_records_key: None,
54 text_content: None,
55 binary_content: None,
56 custom_headers: custom_headers.cloned(),
57 request_headers: request_headers.clone(),
58 confine_to: confine_to.map(Path::to_path_buf),
59 cors_allow_credentials_origins: cors_allow_credentials_origins.to_vec(),
60 }
61 }
62
63 pub fn new_with_csv_records_jsonpath(
65 file_path: &str,
66 custom_headers: Option<&HashMap<String, Option<String>>>,
67 csv_records_key: Option<String>,
68 request_headers: &HeaderMap,
69 confine_to: Option<&Path>,
70 cors_allow_credentials_origins: &[String],
71 ) -> Self {
72 let mut ret = FileResponse::new(
73 file_path,
74 custom_headers,
75 request_headers,
76 confine_to,
77 cors_allow_credentials_origins,
78 );
79 ret.csv_records_key = csv_records_key;
80 ret
81 }
82
83 pub async fn file_content_response(
85 &mut self,
86 ) -> Result<hyper::Response<BoxBody>, hyper::http::Error> {
87 let file_path = match resolve_with_json_compatible_extensions(self.file_path.as_str()) {
88 Some(x) => x,
89 None => {
90 log::warn!(
91 "{}:\n{} (missing or a directory)",
92 style("file not found").red(),
93 self.file_path
94 );
95 return not_found_response(
96 &self.request_headers,
97 &self.cors_allow_credentials_origins,
98 );
99 }
100 };
101
102 let file_path = match confine(file_path.as_str(), self.confine_to.as_deref()) {
108 Some(canonical) => match canonical.to_str() {
109 Some(x) => x.to_owned(),
110 None => {
111 log::error!(
112 "{} to get str from canonicalized file path:\n{}",
113 style("failed").red(),
114 file_path
115 );
116 return not_found_response(
117 &self.request_headers,
118 &self.cors_allow_credentials_origins,
119 );
120 }
121 },
122 None => {
123 return not_found_response(
124 &self.request_headers,
125 &self.cors_allow_credentials_origins,
126 );
127 }
128 };
129 self.file_path = file_path.clone();
130
131 let file_path_to_read = file_path.clone();
141 let content = task::spawn_blocking(move || fs::read(file_path_to_read)).await;
142
143 match content {
144 Ok(Ok(bytes)) => match String::from_utf8(bytes) {
145 Ok(text) => {
146 self.text_content = Some(text);
147 self.text_file_content_response()
148 }
149 Err(err) => {
150 self.binary_content = Some(err.into_bytes());
151 self.binary_content_type_response()
152 }
153 },
154 Ok(Err(err)) => {
155 log::error!("failed to read file ({}): {}", self.file_path, err);
156 internal_server_error_response(
157 "failed to read response file",
158 &self.request_headers,
159 &self.cors_allow_credentials_origins,
160 )
161 }
162 Err(err) => {
163 log::error!("async task failed ({}): {}", self.file_path, err);
164 internal_server_error_response(
165 "failed to read response file",
166 &self.request_headers,
167 &self.cors_allow_credentials_origins,
168 )
169 }
170 }
171 }
172
173 fn text_file_content_response(&self) -> Result<hyper::Response<BoxBody>, hyper::http::Error> {
183 match file_extension(self.file_path.as_str()) {
184 Some(ext) => match ext.as_str() {
185 "json" => self.raw_json_file_content_response(),
192 "json5" => self.json_file_content_response(),
193 "csv" => self.csv_file_content_response(),
194 _ => text_response(
195 self.text_content.clone().unwrap_or_default().as_str(),
196 Some(text_file_content_type(ext).as_str()),
197 self.custom_headers.as_ref(),
198 &self.request_headers,
199 &self.cors_allow_credentials_origins,
200 ),
201 },
202 None => text_response(
203 self.text_content.clone().unwrap_or_default().as_str(),
204 None,
205 self.custom_headers.as_ref(),
206 &self.request_headers,
207 &self.cors_allow_credentials_origins,
208 ),
209 }
210 }
211
212 fn raw_json_file_content_response(
230 &self,
231 ) -> Result<hyper::Response<BoxBody>, hyper::http::Error> {
232 let json_str = self.text_content.clone().unwrap_or_default();
233 ResponseHandler::default()
234 .with_json_body(json_str)
235 .with_custom_headers(self.custom_headers.as_ref())
236 .into_response(&self.request_headers, &self.cors_allow_credentials_origins)
237 }
238
239 fn json_file_content_response(&self) -> Result<hyper::Response<BoxBody>, hyper::http::Error> {
242 let json_str = self.text_content.clone().unwrap_or_default();
243 json_response(
244 json_str.as_str(),
245 None,
246 self.custom_headers.as_ref(),
247 &self.request_headers,
248 Some(self.file_path.as_str()),
249 &self.cors_allow_credentials_origins,
250 )
251 }
252
253 fn csv_file_content_response(&self) -> Result<hyper::Response<BoxBody>, hyper::http::Error> {
255 let text_content = self.text_content.clone().unwrap_or_default();
256 let mut rdr = csv::ReaderBuilder::new()
257 .has_headers(true)
258 .from_reader(text_content.as_bytes());
259
260 let csv_headers = if let Ok(csv_headers) = rdr.headers() {
261 csv_headers.clone()
262 } else {
263 log::error!(
264 "failed to analyze csv headers ({})",
265 self.file_path.as_str()
266 );
267 return internal_server_error_response(
268 "failed to analyze csv headers",
269 &self.request_headers,
270 &self.cors_allow_credentials_origins,
271 );
272 };
273
274 let rows = rdr
275 .records()
276 .map(|result| {
277 let record = result?;
278 let obj = csv_headers
279 .iter()
280 .zip(record.iter())
281 .map(|(k, v)| (k.to_string(), Value::String(v.to_string())))
282 .collect::<Map<_, _>>();
283 Ok(Value::Object(obj))
284 })
285 .collect::<Result<Vec<Value>, csv::Error>>();
286
287 match rows {
288 Ok(rows) => {
289 let jsonpath_key = if let Some(csv_records_key) = self.csv_records_key.as_ref() {
290 csv_records_key.as_str()
291 } else {
292 CSV_RECORDS_DEFAULT_KEY
293 };
294 let json_value = json_value_with_jsonpath_key(jsonpath_key, Value::from(rows));
295
296 let body = serde_json::to_string(&json_value);
297 match body {
298 Ok(body) => json_response(
299 body.as_str(),
300 None,
301 self.custom_headers.as_ref(),
302 &self.request_headers,
303 Some(self.file_path.as_str()),
304 &self.cors_allow_credentials_origins,
305 ),
306 Err(err) => {
307 log::error!(
308 "failed to convert csv records to json response ({}): {}",
309 self.file_path.as_str(),
310 err
311 );
312 internal_server_error_response(
313 "failed to convert csv records to json response",
314 &self.request_headers,
315 &self.cors_allow_credentials_origins,
316 )
317 }
318 }
319 }
320 Err(err) => {
321 log::error!(
322 "failed to analyze csv records ({}): {}",
323 self.file_path.as_str(),
324 err
325 );
326 internal_server_error_response(
327 "failed to analyze csv records",
328 &self.request_headers,
329 &self.cors_allow_credentials_origins,
330 )
331 }
332 }
333 }
334
335 fn binary_content_type_response(&self) -> Result<hyper::Response<BoxBody>, hyper::http::Error> {
344 let content = self.binary_content.clone().unwrap_or_default().to_owned();
345 let content_type = binary_content_type(self.file_path.as_str());
346 ResponseHandler::default()
347 .with_binary_body(content, Some(content_type))
348 .with_custom_headers(self.custom_headers.as_ref())
349 .into_response(&self.request_headers, &self.cors_allow_credentials_origins)
350 }
351}
352
353#[cfg(test)]
354mod tests {
355 use hyper::HeaderMap;
365
366 use super::*;
367 use crate::response::confine::canonical_dir;
368
369 #[tokio::test]
370 async fn invalid_utf8_bytes_are_served_as_binary_regardless_of_a_text_extension() {
371 let dir = tempfile::tempdir().unwrap();
372 let bytes: &[u8] = &[0xFF, 0xFE, 0x00, 0x01, 0x02];
373 let file_path = dir.path().join("weird.txt");
374 std::fs::write(&file_path, bytes).unwrap();
375 let confine_to = canonical_dir(dir.path().to_str().unwrap());
376
377 let mut file_response = FileResponse::new(
378 file_path.to_str().unwrap(),
379 None,
380 &HeaderMap::new(),
381 confine_to.as_deref(),
382 &[],
383 );
384 let response = file_response.file_content_response().await.unwrap();
385
386 assert_eq!(response.status(), hyper::StatusCode::OK);
387 assert_eq!(
388 response.headers().get("content-type").unwrap(),
389 "application/octet-stream",
390 "invalid-UTF-8 bytes must take the binary path even though the \
391 extension says .txt"
392 );
393 let body = http_body_util::BodyExt::collect(response.into_body())
394 .await
395 .unwrap()
396 .to_bytes();
397 assert_eq!(body.as_ref(), bytes, "binary bytes must round-trip exactly");
398 }
399
400 #[tokio::test]
401 async fn valid_utf8_bytes_are_served_as_text_regardless_of_a_binary_extension() {
402 let dir = tempfile::tempdir().unwrap();
403 let text = "hello, this is plain text";
404 let file_path = dir.path().join("weird.bin");
405 std::fs::write(&file_path, text).unwrap();
406 let confine_to = canonical_dir(dir.path().to_str().unwrap());
407
408 let mut file_response = FileResponse::new(
409 file_path.to_str().unwrap(),
410 None,
411 &HeaderMap::new(),
412 confine_to.as_deref(),
413 &[],
414 );
415 let response = file_response.file_content_response().await.unwrap();
416
417 assert_eq!(response.status(), hyper::StatusCode::OK);
418 assert_eq!(
419 response.headers().get("content-type").unwrap(),
420 "text/plain; charset=utf-8",
421 "valid-UTF-8 bytes must take the text path even though the \
422 extension says .bin"
423 );
424 let body = http_body_util::BodyExt::collect(response.into_body())
425 .await
426 .unwrap()
427 .to_bytes();
428 assert_eq!(body.as_ref(), text.as_bytes());
429 }
430
431 #[tokio::test]
436 async fn json_file_is_served_byte_identical_key_order_and_formatting_preserved() {
437 let dir = tempfile::tempdir().unwrap();
438 let bytes: &[u8] = b"{\n \"zebra\": 1,\n \"apple\": 2\n}\n";
439 let file_path = dir.path().join("data.json");
440 std::fs::write(&file_path, bytes).unwrap();
441 let confine_to = canonical_dir(dir.path().to_str().unwrap());
442
443 let mut file_response = FileResponse::new(
444 file_path.to_str().unwrap(),
445 None,
446 &HeaderMap::new(),
447 confine_to.as_deref(),
448 &[],
449 );
450 let response = file_response.file_content_response().await.unwrap();
451
452 assert_eq!(response.status(), hyper::StatusCode::OK);
453 assert_eq!(
454 response.headers().get("content-type").unwrap(),
455 "application/json"
456 );
457 let body = http_body_util::BodyExt::collect(response.into_body())
458 .await
459 .unwrap()
460 .to_bytes();
461 assert_eq!(
462 body.as_ref(),
463 bytes,
464 "a .json file must be served exactly as written — not \
465 minified, not key-reordered"
466 );
467 }
468
469 #[tokio::test]
474 async fn json5_file_still_converts_to_minified_json() {
475 let dir = tempfile::tempdir().unwrap();
476 let source = b"{\n \"zebra\": 1,\n \"apple\": 2,\n}\n";
480 let file_path = dir.path().join("data.json5");
481 std::fs::write(&file_path, source).unwrap();
482 let confine_to = canonical_dir(dir.path().to_str().unwrap());
483
484 let mut file_response = FileResponse::new(
485 file_path.to_str().unwrap(),
486 None,
487 &HeaderMap::new(),
488 confine_to.as_deref(),
489 &[],
490 );
491 let response = file_response.file_content_response().await.unwrap();
492
493 assert_eq!(response.status(), hyper::StatusCode::OK);
494 let body = http_body_util::BodyExt::collect(response.into_body())
495 .await
496 .unwrap()
497 .to_bytes();
498 assert_ne!(
502 body.as_ref(),
503 source,
504 ".json5 must still be converted, not served raw"
505 );
506 let parsed: serde_json::Value = serde_json::from_slice(&body).unwrap();
507 assert_eq!(parsed["zebra"], 1);
508 assert_eq!(parsed["apple"], 2);
509 }
510}