Skip to main content

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::base::RUMResult;
22use rumtk_core::buffers::buffer_to_string;
23use rumtk_core::buffers::*;
24use rumtk_core::strings::{
25    rumtk_format, RUMString,
26};
27use rumtk_core::types::{RUMHashMap, RUMID};
28
29use crate::utils::defaults::*;
30use crate::{RUMWebData, RouterForm};
31
32pub type FormBuffer = RUMBuffer;
33
34#[derive(Default, Debug, PartialEq, Clone)]
35pub struct FormData {
36    pub form: RUMWebData,
37    pub files: RUMHashMap<RUMString, FormBuffer>,
38}
39
40impl FormData {
41    pub fn len(&self) -> usize {
42        self.form.len()
43    }
44    
45    pub fn is_empty(&self) -> bool {
46        self.form.is_empty()
47    }
48}
49
50pub type FormResult = RUMResult<FormData>;
51
52pub async fn get_type(content_type: &str) -> &'static str {
53    match content_type {
54        CONTENT_TYPE_PDF => FORM_DATA_TYPE_PDF,
55        _ => FORM_DATA_TYPE_DEFAULT,
56    }
57}
58
59///
60/// Converts the incoming form data with type [RouterForm] to [FormData] which is the preferred
61/// type in the library.
62///
63/// ## Examples
64///
65/// ### Plaintext only
66/// ```
67/// use axum::body::Body;
68/// use rumtk_core::{rumtk_spawn_task, rumtk_resolve_task};
69/// use rumtk_web::utils::testdata::data::TESTDATA_FORMDATA_REQUEST;
70/// use rumtk_web::utils::RouterForm;
71/// use rumtk_web::utils::form_data::compile_form_data;
72/// use rumtk_web::FormData;
73/// use axum::extract::{Request, FromRequest};
74/// use rumtk_core::base::RUMResult;
75/// use rumtk_core::buffers::*;
76/// use rumtk_web::form_data::FormResult;
77///
78/// let expected_form = FormData::default();
79///
80/// async fn create_form() -> FormResult {
81///     let mut raw_form = RouterForm::from_request(TESTDATA_FORMDATA_REQUEST(), &()).await.expect("Multipart form expected.");
82///     compile_form_data(&mut raw_form).await
83/// }
84///
85/// rumtk_resolve_task!(create_form());
86///
87/// ```
88///
89/// ## Note
90/// ```text
91/// Because anything that axum does not like could trigger a truncation of the incoming form, I
92/// could not even test this function without silencing the parsing error and returning any successful
93/// results so far. Axum would complain about an error parsing the multipart form when using a "mocked" Body buffer.
94/// Turns out, you can still properly parse a buffer. Also, for testing purposes, you cannot use byte
95/// literals as the input to a mocked Body but you can use a Vec<u8> and write!() to it then call
96/// into() on that buffer and everything then works despite still complaining about the error.
97/// Since we are ignoring anything past this point, I think this is technically safe while still
98/// allowing us to test this logic.
99/// ```
100///
101pub async fn compile_form_data(form: &mut RouterForm) -> FormResult {
102    let mut form_data = FormData::default();
103    
104    while let field_result = form.next_field().await {
105        match field_result {
106            Ok(field_option) => match field_option {
107                Some(mut field) => {
108                    let typ = match field.content_type() {
109                        Some(content_type) => get_type(content_type).await,
110                        None => FORM_DATA_TYPE_DEFAULT,
111                    };
112                    let name = field.name().unwrap_or_default().to_string();
113                    
114                    // If we got an empty field name, discard.
115                    if name.is_empty() {
116                        continue;
117                    }
118
119                    let data = match field.bytes().await {
120                        Ok(bytes) => bytes,
121                        Err(e) => {
122                            return Err(rumtk_format!("Field data transfer failed because {}!", e))
123                        }
124                    };
125
126                    if typ == FORM_DATA_TYPE_DEFAULT {
127                        form_data.form.insert(name, buffer_to_string(data.as_slice())?);
128                    } else {
129                        let file_id = RUMID::new_v4().to_string();
130                        &form_data.files.insert(file_id.clone(), FormBuffer::from(data.as_slice()));
131                        &form_data.form.insert(name, file_id);
132                    }
133                }
134                None => {
135                    // Ok so this one is important to be careful with.
136                    // During some testing, I was able to pass a form with no fields. 
137                    // This form is not 0 bytes as it does contain one boundary line and then the 
138                    // new line. However, unlike during the browser test, it was not possible to 
139                    // craft a unit test that replicates the issue perhaps because the unit test has 
140                    // a fixed buffer? Not sure, it is strange. Without a unit test, it is not clear
141                    // how to report issue to axum. At any rate, we should be handling this bit 
142                    // anyways. So if we encounter a None for the next field, let's assume the form 
143                    // is complete.
144                    break;
145                }
146            },
147            Err(e) => {
148                // Just return what you got. This is tricky, because anything that axum does not like could
149                // trigger a truncation of the incoming form, but I could not even test this function without
150                // doing this because it would complain about an error parsing the multipart form. Turns out,
151                // you can still properly parse a buffer. Also, for testing purposes, you cannot use byte
152                // literals as the input to a mocked Body but you can use a Vec<u8> and write!() to it then
153                // call into() on that buffer and everything then works despite still complaining about the error.
154                // Since we are ignoring anything past this point, I think this is technically safe while still
155                // allowing us to test this logic.
156                return Ok(form_data);
157            }
158        }
159    }
160
161    Ok(form_data)
162}