rumtk_web/utils/form_data.rs
1/*
2 * rumtk attempts to implement HL7 and medical protocols for interoperability in medicine.
3 * This toolkit aims to be reliable, simple, performant, and standards compliant.
4 * Copyright (C) 2025 Luis M. Santos, M.D. <lsantos@medicalmasses.com>
5 * Copyright (C) 2025 Ethan Dixon
6 * Copyright (C) 2025 MedicalMasses L.L.C. <contact@medicalmasses.com>
7 *
8 * This program is free software: you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation, either version 3 of the License, or
11 * (at your option) any later version.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License
19 * along with this program. If not, see <https://www.gnu.org/licenses/>.
20 */
21use rumtk_core::buffers::buffer_to_string;
22use rumtk_core::core::RUMResult;
23use rumtk_core::strings::{
24 rumtk_format, RUMString,
25};
26use rumtk_core::types::{RUMBuffer, RUMHashMap, RUMID};
27
28use crate::utils::defaults::*;
29use crate::{RUMWebData, RouterForm};
30
31pub type FormBuffer = RUMBuffer;
32
33#[derive(Default, Debug, PartialEq, Clone)]
34pub struct FormData {
35 pub form: RUMWebData,
36 pub files: RUMHashMap<RUMString, FormBuffer>,
37}
38
39impl FormData {
40 pub fn len(&self) -> usize {
41 self.form.len()
42 }
43
44 pub fn is_empty(&self) -> bool {
45 self.form.is_empty()
46 }
47}
48
49pub type FormResult = RUMResult<FormData>;
50
51pub async fn get_type(content_type: &str) -> &'static str {
52 match content_type {
53 CONTENT_TYPE_PDF => FORM_DATA_TYPE_PDF,
54 _ => FORM_DATA_TYPE_DEFAULT,
55 }
56}
57
58///
59/// Converts the incoming form data with type [RouterForm] to [FormData] which is the preferred
60/// type in the library.
61///
62/// ## Examples
63///
64/// ### Plaintext only
65/// ```
66/// use axum::body::Body;
67/// use rumtk_core::{rumtk_spawn_task, rumtk_resolve_task};
68/// use rumtk_web::utils::testdata::data::TESTDATA_FORMDATA_REQUEST;
69/// use rumtk_web::utils::RouterForm;
70/// use rumtk_web::utils::form_data::compile_form_data;
71/// use rumtk_web::FormData;
72/// use axum::extract::{Request, FromRequest};
73/// use rumtk_core::core::RUMResult;
74/// use rumtk_core::types::RUMBuffer;
75/// use rumtk_web::form_data::FormResult;
76///
77/// let expected_form = FormData::default();
78///
79/// async fn create_form() -> FormResult {
80/// let mut raw_form = RouterForm::from_request(TESTDATA_FORMDATA_REQUEST(), &()).await.expect("Multipart form expected.");
81/// compile_form_data(&mut raw_form).await
82/// }
83///
84/// rumtk_resolve_task!(create_form());
85///
86/// ```
87///
88/// ## Note
89/// ```text
90/// Because anything that axum does not like could trigger a truncation of the incoming form, I
91/// could not even test this function without silencing the parsing error and returning any successful
92/// results so far. Axum would complain about an error parsing the multipart form when using a "mocked" Body buffer.
93/// Turns out, you can still properly parse a buffer. Also, for testing purposes, you cannot use byte
94/// literals as the input to a mocked Body but you can use a Vec<u8> and write!() to it then call
95/// into() on that buffer and everything then works despite still complaining about the error.
96/// Since we are ignoring anything past this point, I think this is technically safe while still
97/// allowing us to test this logic.
98/// ```
99///
100pub async fn compile_form_data(form: &mut RouterForm) -> FormResult {
101 let mut form_data = FormData::default();
102
103 while let field_result = form.next_field().await {
104 match field_result {
105 Ok(field_option) => match field_option {
106 Some(mut field) => {
107 let typ = match field.content_type() {
108 Some(content_type) => get_type(content_type).await,
109 None => FORM_DATA_TYPE_DEFAULT,
110 };
111 let name = field.name().unwrap_or_default().to_string();
112
113 // If we got an empty field name, discard.
114 if name.is_empty() {
115 continue;
116 }
117
118 let data = match field.bytes().await {
119 Ok(bytes) => bytes,
120 Err(e) => {
121 return Err(rumtk_format!("Field data transfer failed because {}!", e))
122 }
123 };
124
125 if typ == FORM_DATA_TYPE_DEFAULT {
126 form_data.form.insert(name, buffer_to_string(data.as_slice())?);
127 } else {
128 let file_id = RUMID::new_v4().to_string();
129 &form_data.files.insert(file_id.clone(), data);
130 &form_data.form.insert(name, file_id);
131 }
132 }
133 None => {
134 // Ok so this one is important to be careful with.
135 // During some testing, I was able to pass a form with no fields.
136 // This form is not 0 bytes as it does contain one boundary line and then the
137 // new line. However, unlike during the browser test, it was not possible to
138 // craft a unit test that replicates the issue perhaps because the unit test has
139 // a fixed buffer? Not sure, it is strange. Without a unit test, it is not clear
140 // how to report issue to axum. At any rate, we should be handling this bit
141 // anyways. So if we encounter a None for the next field, let's assume the form
142 // is complete.
143 break;
144 }
145 },
146 Err(e) => {
147 // Just return what you got. This is tricky, because anything that axum does not like could
148 // trigger a truncation of the incoming form, but I could not even test this function without
149 // doing this because it would complain about an error parsing the multipart form. Turns out,
150 // you can still properly parse a buffer. Also, for testing purposes, you cannot use byte
151 // literals as the input to a mocked Body but you can use a Vec<u8> and write!() to it then
152 // call into() on that buffer and everything then works despite still complaining about the error.
153 // Since we are ignoring anything past this point, I think this is technically safe while still
154 // allowing us to test this logic.
155 return Ok(form_data);
156 }
157 }
158 }
159
160 Ok(form_data)
161}