فهرست منبع

Add public MultipartFormDataWriter for multipart body serialization

Building a multipart/form-data body outside Client::Post/Put/Patch
(e.g. to feed a custom content provider or compose with other body
sources) previously required calling several detail:: functions
directly, which is not a stable interface.

Introduce MultipartFormDataWriter, a small public class that owns the
boundary and delegates to the existing detail:: serializers: whole-body
serialization with known content length, and per-part framing
(item_begin/item_end/finish) for streaming. Also expose
is_valid_multipart_boundary() so callers using an explicit boundary can
validate it without exceptions.
yhirose 3 ماه پیش
والد
کامیت
9e8b960e44
2فایلهای تغییر یافته به همراه157 افزوده شده و 0 حذف شده
  1. 70 0
      httplib.h
  2. 87 0
      test/test.cc

+ 70 - 0
httplib.h

@@ -1680,6 +1680,34 @@ make_multipart_content_provider(const UploadFormDataItems &items,
 
 } // namespace detail
 
+bool is_valid_multipart_boundary(const std::string &boundary);
+
+// Serializer for multipart/form-data request bodies. The boundary is owned
+// by the writer so that per-part framing and the final terminator always
+// agree. Field names and filenames are escaped following the WHATWG HTML
+// standard ('"' -> %22, CR -> %0D, LF -> %0A).
+class MultipartFormDataWriter {
+public:
+  MultipartFormDataWriter();
+  // precondition: is_valid_multipart_boundary(boundary)
+  explicit MultipartFormDataWriter(std::string boundary);
+
+  const std::string &boundary() const;
+  std::string content_type() const;
+
+  // In-memory items -> whole body (known length)
+  std::string serialize(const UploadFormDataItems &items) const;
+  size_t content_length(const UploadFormDataItems &items) const;
+
+  // Per-part framing for streaming via a content provider
+  std::string item_begin(const UploadFormData &item) const;
+  static std::string item_end();
+  std::string finish() const;
+
+private:
+  std::string boundary_;
+};
+
 class Server {
 public:
   using Handler = std::function<void(const Request &, Response &)>;
@@ -10002,6 +10030,48 @@ inline size_t MultipartFormData::get_file_count(const std::string &key) const {
   return static_cast<size_t>(std::distance(r.first, r.second));
 }
 
+// Multipart FormData writer implementation
+inline bool is_valid_multipart_boundary(const std::string &boundary) {
+  return detail::is_multipart_boundary_chars_valid(boundary);
+}
+
+inline MultipartFormDataWriter::MultipartFormDataWriter()
+    : boundary_(detail::make_multipart_data_boundary()) {}
+
+inline MultipartFormDataWriter::MultipartFormDataWriter(std::string boundary)
+    : boundary_(std::move(boundary)) {}
+
+inline const std::string &MultipartFormDataWriter::boundary() const {
+  return boundary_;
+}
+
+inline std::string MultipartFormDataWriter::content_type() const {
+  return detail::serialize_multipart_formdata_get_content_type(boundary_);
+}
+
+inline std::string
+MultipartFormDataWriter::serialize(const UploadFormDataItems &items) const {
+  return detail::serialize_multipart_formdata(items, boundary_);
+}
+
+inline size_t MultipartFormDataWriter::content_length(
+    const UploadFormDataItems &items) const {
+  return detail::get_multipart_content_length(items, boundary_);
+}
+
+inline std::string
+MultipartFormDataWriter::item_begin(const UploadFormData &item) const {
+  return detail::serialize_multipart_formdata_item_begin(item, boundary_);
+}
+
+inline std::string MultipartFormDataWriter::item_end() {
+  return detail::serialize_multipart_formdata_item_end();
+}
+
+inline std::string MultipartFormDataWriter::finish() const {
+  return detail::serialize_multipart_formdata_finish(boundary_);
+}
+
 // Response implementation
 inline size_t Response::get_header_value_u64(const std::string &key, size_t def,
                                              size_t id) const {

+ 87 - 0
test/test.cc

@@ -6591,6 +6591,93 @@ TEST(MultipartFormDataTest, FieldEscaping) {
   EXPECT_EQ(StatusCode::OK_200, res->status);
 }
 
+TEST(MultipartFormDataTest, PublicWriterAPI) {
+  const UploadFormDataItems items = {
+      {"name1", "Content 1", "", ""},
+      {"name2", "Content 2", "file2.txt", "text/plain"},
+  };
+
+  Server svr;
+
+  svr.Post("/post", [&](const Request &req, Response & /*res*/,
+                        const ContentReader &content_reader) {
+    ASSERT_TRUE(req.is_multipart_form_data());
+    std::vector<FormData> received;
+    content_reader(
+        [&](const FormData &file) {
+          received.push_back(file);
+          return true;
+        },
+        [&](const char *data, size_t data_length) {
+          received.back().content.append(data, data_length);
+          return true;
+        });
+
+    ASSERT_EQ(2U, received.size());
+    EXPECT_EQ("name1", received[0].name);
+    EXPECT_EQ("Content 1", received[0].content);
+    EXPECT_EQ("name2", received[1].name);
+    EXPECT_EQ("Content 2", received[1].content);
+    EXPECT_EQ("file2.txt", received[1].filename);
+    EXPECT_EQ("text/plain", received[1].content_type);
+  });
+
+  auto port = svr.bind_to_any_port("localhost");
+  auto t = std::thread([&]() { svr.listen_after_bind(); });
+  auto se = detail::scope_exit([&] {
+    svr.stop();
+    t.join();
+    ASSERT_FALSE(svr.is_running());
+  });
+
+  svr.wait_until_ready();
+
+  Client cli("localhost", port);
+
+  // Whole-body serialization with a generated boundary
+  {
+    MultipartFormDataWriter writer;
+    EXPECT_TRUE(is_valid_multipart_boundary(writer.boundary()));
+    EXPECT_EQ("multipart/form-data; boundary=" + writer.boundary(),
+              writer.content_type());
+
+    auto body = writer.serialize(items);
+    EXPECT_EQ(body.size(), writer.content_length(items));
+
+    auto res = cli.Post("/post", body, writer.content_type());
+    ASSERT_TRUE(res);
+    EXPECT_EQ(StatusCode::OK_200, res->status);
+  }
+
+  // Per-part framing with a custom boundary via a content provider
+  {
+    EXPECT_FALSE(is_valid_multipart_boundary("bad boundary"));
+    ASSERT_TRUE(is_valid_multipart_boundary("custom-boundary_123"));
+
+    MultipartFormDataWriter writer("custom-boundary_123");
+    EXPECT_EQ("custom-boundary_123", writer.boundary());
+
+    std::string body;
+    for (const auto &item : items) {
+      body += writer.item_begin(item);
+      body += item.content;
+      body += MultipartFormDataWriter::item_end();
+    }
+    body += writer.finish();
+    EXPECT_EQ(body.size(), writer.content_length(items));
+
+    auto res = cli.Post(
+        "/post", body.size(),
+        [&](size_t offset, size_t length, DataSink &sink) {
+          sink.write(body.data() + offset, length);
+          return true;
+        },
+        writer.content_type());
+    ASSERT_TRUE(res);
+    EXPECT_EQ(StatusCode::OK_200, res->status);
+  }
+}
+
 TEST_F(ServerTest, PostContentReceiverGzip) {
   cli_.set_compress(true);
   auto res = cli_.Post("/content_receiver", "content", "text/plain");