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") {