diff --git a/CMakeLists.txt b/CMakeLists.txt
index f411bd959..ef41b175c 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -19,7 +19,7 @@ if (CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR)
endif()
# Set required C++ standard
-set(CMAKE_CXX_STANDARD 11)
+set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED TRUE)
# Default to build type "Release" unless tests are being built
diff --git a/docs/guides/multipart.md b/docs/guides/multipart.md
index 5761e6af2..53adaae86 100644
--- a/docs/guides/multipart.md
+++ b/docs/guides/multipart.md
@@ -17,7 +17,7 @@ The structure of a multipart request is typically consistent of:
- `----`
## Multipart messages in Crow
-Crow supports multipart requests and responses though `crow::multipart::message`.
+Crow supports multipart requests and responses though `crow::multipart::message` and `crow::multipart::message_view`, where `crow::multipart::message` owns the contents of the message and `crow::multipart::message_view` stores views into its parts.
A message can be created either by defining the headers, boundary, and individual parts and using them to create the message. or simply by reading a `crow::request`.
Once a multipart message has been made, the individual parts can be accessed throughout `msg.parts`, `parts` is an `std::vector`.
@@ -25,7 +25,7 @@ Once a multipart message has been made, the individual parts can be accessed thr
[:octicons-feed-tag-16: v1.0](https://github.com/CrowCpp/Crow/releases/v1.0)
-Part headers are organized in a similar way to request and response headers, and can be retrieved via `crow::multipart::get_header_object("header-key")`. This function returns a `crow::multipart::header` object.
+Part headers are organized in a similar way to request and response headers, and can be retrieved via `crow::multipart::get_header_object("header-key")`. This function returns a `crow::multipart::header` object for owning message and `crow::multipart::header_view` for non-owning message.
The message's individual body parts can be accessed by name using `msg.get_part_by_name("part-name")`.
diff --git a/examples/example.cpp b/examples/example.cpp
index f1bb44ff0..26488fcfd 100644
--- a/examples/example.cpp
+++ b/examples/example.cpp
@@ -210,7 +210,7 @@ int main()
// Take a multipart/form-data request and print out its body
CROW_ROUTE(app, "/multipart")
([](const crow::request& req) {
- crow::multipart::message msg(req);
+ crow::multipart::message_view msg(req);
CROW_LOG_INFO << "body of the first part " << msg.parts[0].body;
return "it works!";
});
diff --git a/examples/example_file_upload.cpp b/examples/example_file_upload.cpp
index bf78b0cc0..89985e507 100644
--- a/examples/example_file_upload.cpp
+++ b/examples/example_file_upload.cpp
@@ -6,7 +6,7 @@ int main()
CROW_ROUTE(app, "/uploadfile")
.methods(crow::HTTPMethod::Post)([](const crow::request& req) {
- crow::multipart::message file_message(req);
+ crow::multipart::message_view file_message(req);
for (const auto& part : file_message.part_map)
{
const auto& part_name = part.first;
@@ -27,7 +27,7 @@ int main()
CROW_LOG_ERROR << "Part with name \"InputFile\" should have a file";
return crow::response(400);
}
- const std::string outfile_name = params_it->second;
+ const std::string outfile_name{params_it->second};
for (const auto& part_header : part_value.headers)
{
diff --git a/include/crow.h b/include/crow.h
index 25c136ad6..e2afd8ac8 100644
--- a/include/crow.h
+++ b/include/crow.h
@@ -16,6 +16,7 @@
#include "crow/parser.h"
#include "crow/http_response.h"
#include "crow/multipart.h"
+#include "crow/multipart_view.h"
#include "crow/routing.h"
#include "crow/middleware.h"
#include "crow/middleware_context.h"
diff --git a/include/crow/ci_map.h b/include/crow/ci_map.h
index ab18e3ddf..71962598a 100644
--- a/include/crow/ci_map.h
+++ b/include/crow/ci_map.h
@@ -1,7 +1,9 @@
#pragma once
+#include
#include
#include
+
#include "crow/utility.h"
namespace crow
@@ -9,7 +11,7 @@ namespace crow
/// Hashing function for ci_map (unordered_multimap).
struct ci_hash
{
- size_t operator()(const std::string& key) const
+ size_t operator()(const std::string_view key) const
{
std::size_t seed = 0;
std::locale locale;
@@ -31,7 +33,7 @@ namespace crow
/// Equals function for ci_map (unordered_multimap).
struct ci_key_eq
{
- bool operator()(const std::string& l, const std::string& r) const
+ bool operator()(const std::string_view l, const std::string_view r) const
{
return utility::string_equals(l, r);
}
diff --git a/include/crow/multipart_view.h b/include/crow/multipart_view.h
new file mode 100644
index 000000000..86df5b338
--- /dev/null
+++ b/include/crow/multipart_view.h
@@ -0,0 +1,312 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+
+#include "crow/http_request.h"
+// for crow::multipart::dd
+#include "crow/multipart.h"
+#include "crow/ci_map.h"
+
+namespace crow
+{
+
+ /// Encapsulates anything related to processing and organizing `multipart/xyz` messages
+ namespace multipart
+ {
+ /// The first part in a section, contains metadata about the part
+ struct header_view
+ {
+ std::string_view value; ///< The first part of the header, usually `Content-Type` or `Content-Disposition`
+ std::unordered_map params; ///< The parameters of the header, come after the `value`
+
+ /// Returns \ref value as integer
+ operator int() const
+ {
+ int result = 0;
+ std::from_chars(value.data(), value.data() + value.size(), result);
+ return result;
+ }
+
+ /// Returns \ref value as double
+ operator double() const
+ {
+ // There's no std::from_chars for floating-point types in a lot of STLs
+ return std::stod(static_cast(value));
+ }
+ };
+
+ /// Multipart header map (key is header key).
+ using mph_view_map = std::unordered_multimap;
+
+ /// Finds and returns the header with the specified key. (returns an empty header if nothing is found)
+ inline const header_view& get_header_object(const mph_view_map& headers, const std::string_view key)
+ {
+ const auto header = headers.find(key);
+ if (header != headers.cend())
+ {
+ return header->second;
+ }
+
+ static header_view empty;
+ return empty;
+ }
+
+ /// String padded with the specified padding (double quotes by default)
+ struct padded
+ {
+ std::string_view value; ///< String to pad
+ const char padding = '"'; ///< Padding to use
+
+ /// Outputs padded value to the stream
+ friend std::ostream& operator<<(std::ostream& stream, const padded value)
+ {
+ return stream << value.padding << value.value << value.padding;
+ }
+ };
+
+ ///One part of the multipart message
+
+ ///
+ /// It is usually separated from other sections by a `boundary`
+ struct part_view
+ {
+ mph_view_map headers; ///< (optional) The first part before the data, Contains information regarding the type of data and encoding
+ std::string_view body; ///< The actual data in the part
+
+ /// Returns \ref body as integer
+ operator int() const
+ {
+ int result = 0;
+ std::from_chars(body.data(), body.data() + body.size(), result);
+ return result;
+ }
+
+ /// Returns \ref body as double
+ operator double() const
+ {
+ // There's no std::from_chars for floating-point types in a lot of STLs
+ return std::stod(static_cast(body));
+ }
+
+ const header_view& get_header_object(const std::string_view key) const
+ {
+ return multipart::get_header_object(headers, key);
+ }
+
+ friend std::ostream& operator<<(std::ostream& stream, const part_view& part)
+ {
+ for (const auto& [key, value] : part.headers)
+ {
+ stream << key << ": " << value.value;
+ for (const auto& [key, value] : value.params)
+ {
+ stream << "; " << key << '=' << padded{value};
+ }
+ stream << crlf;
+ }
+ stream << crlf;
+ stream << part.body << crlf;
+ return stream;
+ }
+ };
+
+ /// Multipart map (key is the name parameter).
+ using mp_view_map = std::unordered_multimap;
+
+ /// The parsed multipart request/response
+ struct message_view
+ {
+ std::reference_wrapper headers; ///< The request/response headers
+ std::string boundary; ///< The text boundary that separates different `parts`
+ std::vector parts; ///< The individual parts of the message
+ mp_view_map part_map; ///< The individual parts of the message, organized in a map with the `name` header parameter being the key
+
+ const std::string& get_header_value(const std::string& key) const
+ {
+ return crow::get_header_value(headers.get(), key);
+ }
+
+ part_view get_part_by_name(const std::string_view name)
+ {
+ mp_view_map::iterator result = part_map.find(name);
+ if (result != part_map.end())
+ return result->second;
+ else
+ return {};
+ }
+
+ friend std::ostream& operator<<(std::ostream& stream, const message_view message)
+ {
+ std::string delimiter = dd + message.boundary;
+
+ for (const part_view& part : message.parts)
+ {
+ stream << delimiter << crlf;
+ stream << part;
+ }
+ stream << delimiter << dd << crlf;
+
+ return stream;
+ }
+
+ /// Represent all parts as a string (**does not include message headers**)
+ std::string dump() const
+ {
+ std::ostringstream str;
+ str << *this;
+ return std::move(str).str();
+ }
+
+ /// Represent an individual part as a string
+ std::string dump(int part_) const
+ {
+ std::ostringstream str;
+ str << parts.at(part_);
+ return std::move(str).str();
+ }
+
+ /// Default constructor using default values
+ message_view(const ci_map& headers, const std::string& boundary, const std::vector& sections):
+ headers(headers), boundary(boundary), parts(sections)
+ {
+ for (const part_view& item : parts)
+ {
+ part_map.emplace(
+ (get_header_object(item.headers, "Content-Disposition").params.find("name")->second),
+ item);
+ }
+ }
+
+ /// Create a multipart message from a request data
+ explicit message_view(const request& req):
+ headers(req.headers),
+ boundary(get_boundary(get_header_value("Content-Type")))
+ {
+ parse_body(req.body, parts, part_map);
+ }
+
+ private:
+ std::string_view get_boundary(const std::string_view header) const
+ {
+ constexpr std::string_view boundary_text = "boundary=";
+ const size_t found = header.find(boundary_text);
+ if (found == std::string_view::npos)
+ {
+ return std::string_view();
+ }
+
+ const std::string_view to_return = header.substr(found + boundary_text.size());
+ if (to_return[0] == '\"')
+ {
+ return to_return.substr(1, to_return.length() - 2);
+ }
+ return to_return;
+ }
+
+ void parse_body(std::string_view body, std::vector& sections, mp_view_map& part_map)
+ {
+ const std::string delimiter = dd + boundary;
+
+ // TODO(EDev): Exit on error
+ while (body != (crlf))
+ {
+ const size_t found = body.find(delimiter);
+ if (found == std::string_view::npos)
+ {
+ // did not find delimiter; probably an ill-formed body; ignore the rest
+ break;
+ }
+
+ const std::string_view section = body.substr(0, found);
+
+ // +2 is the CRLF.
+ // We don't check it and delete it so that the same delimiter can be used for The last delimiter (--delimiter--CRLF).
+ body = body.substr(found + delimiter.length() + 2);
+ if (!section.empty())
+ {
+ part_view parsed_section = parse_section(section);
+ part_map.emplace(
+ (get_header_object(parsed_section.headers, "Content-Disposition").params.find("name")->second),
+ parsed_section);
+ sections.push_back(std::move(parsed_section));
+ }
+ }
+ }
+
+ part_view parse_section(std::string_view section)
+ {
+ constexpr static std::string_view crlf2 = "\r\n\r\n";
+
+ const size_t found = section.find(crlf2);
+ const std::string_view head_line = section.substr(0, found + 2);
+ section = section.substr(found + 4);
+
+ return part_view{
+ parse_section_head(head_line),
+ section.substr(0, section.length() - 2),
+ };
+ }
+
+ mph_view_map parse_section_head(std::string_view lines)
+ {
+ mph_view_map result;
+
+ while (!lines.empty())
+ {
+ header_view to_add;
+
+ const size_t found = lines.find(crlf);
+ std::string_view line = lines.substr(0, found);
+ std::string_view key;
+ lines = lines.substr(found + 2);
+ // Add the header if available
+ if (!line.empty())
+ {
+ const size_t found = line.find("; ");
+ std::string_view header = line.substr(0, found);
+ if (found != std::string_view::npos)
+ line = line.substr(found + 2);
+ else
+ line = std::string_view();
+
+ const size_t header_split = header.find(": ");
+ key = header.substr(0, header_split);
+
+ to_add.value = header.substr(header_split + 2);
+ }
+
+ // Add the parameters
+ while (!line.empty())
+ {
+ const size_t found = line.find("; ");
+ std::string_view param = line.substr(0, found);
+ if (found != std::string_view::npos)
+ line = line.substr(found + 2);
+ else
+ line = std::string_view();
+
+ const size_t param_split = param.find('=');
+
+ const std::string_view value = param.substr(param_split + 1);
+
+ to_add.params.emplace(param.substr(0, param_split), trim(value));
+ }
+ result.emplace(key, to_add);
+ }
+
+ return result;
+ }
+
+ inline std::string_view trim(const std::string_view string, const char excess = '"') const
+ {
+ if (string.length() > 1 && string[0] == excess && string[string.length() - 1] == excess)
+ return string.substr(1, string.length() - 2);
+ return string;
+ }
+ };
+ } // namespace multipart
+} // namespace crow
diff --git a/include/crow/utility.h b/include/crow/utility.h
index 7be9a814c..21bdeb6eb 100644
--- a/include/crow/utility.h
+++ b/include/crow/utility.h
@@ -8,6 +8,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -826,7 +827,7 @@ namespace crow
* Always returns false if strings differ in size.
* Defaults to case-insensitive comparison.
*/
- inline static bool string_equals(const std::string& l, const std::string& r, bool case_sensitive = false)
+ inline static bool string_equals(const std::string_view l, const std::string_view r, bool case_sensitive = false)
{
if (l.length() != r.length())
return false;
diff --git a/tests/unittest.cpp b/tests/unittest.cpp
index a3fddae72..3d07cda8e 100644
--- a/tests/unittest.cpp
+++ b/tests/unittest.cpp
@@ -2484,6 +2484,72 @@ TEST_CASE("multipart")
}
} // multipart
+TEST_CASE("multipart_view")
+{
+ //
+ //--CROW-BOUNDARY
+ //Content-Disposition: form-data; name=\"hello\"
+ //
+ //world
+ //--CROW-BOUNDARY
+ //Content-Disposition: form-data; name=\"world\"
+ //
+ //hello
+ //--CROW-BOUNDARY
+ //Content-Disposition: form-data; name=\"multiline\"
+ //
+ //text
+ //text
+ //text
+ //--CROW-BOUNDARY--
+ //
+
+ std::string test_string = "--CROW-BOUNDARY\r\nContent-Disposition: form-data; name=\"hello\"\r\n\r\nworld\r\n--CROW-BOUNDARY\r\nContent-Disposition: form-data; name=\"world\"\r\n\r\nhello\r\n--CROW-BOUNDARY\r\nContent-Disposition: form-data; name=\"multiline\"\r\n\r\ntext\ntext\ntext\r\n--CROW-BOUNDARY--\r\n";
+
+ SimpleApp app;
+
+ CROW_ROUTE(app, "/multipart")
+ ([](const crow::request& req, crow::response& res) {
+ multipart::message_view msg(req);
+ res.body = msg.dump();
+ res.end();
+ });
+
+ app.validate();
+
+ {
+ request req;
+ response res;
+
+ req.url = "/multipart";
+ req.add_header("Content-Type", "multipart/form-data; boundary=CROW-BOUNDARY");
+ req.body = test_string;
+
+ app.handle_full(req, res);
+
+ CHECK(test_string == res.body);
+
+
+ multipart::message_view msg(req);
+ CHECK("hello" == msg.get_part_by_name("world").body);
+ CHECK("form-data" == msg.get_part_by_name("hello").get_header_object("Content-Disposition").value);
+ }
+
+ // Check with `"` encapsulating the boundary (.NET clients do this)
+ {
+ request req;
+ response res;
+
+ req.url = "/multipart";
+ req.add_header("Content-Type", "multipart/form-data; boundary=\"CROW-BOUNDARY\"");
+ req.body = test_string;
+
+ app.handle_full(req, res);
+
+ CHECK(test_string == res.body);
+ }
+} // multipart_view
+
TEST_CASE("send_file")
{