diff --git a/wpiutil/src/main/native/cpp/HttpUtil.cpp b/wpiutil/src/main/native/cpp/HttpUtil.cpp index 284b91535b..37fea6014d 100644 --- a/wpiutil/src/main/native/cpp/HttpUtil.cpp +++ b/wpiutil/src/main/native/cpp/HttpUtil.cpp @@ -333,4 +333,78 @@ bool HttpConnection::Handshake(const HttpRequest& request, return true; } +void HttpMultipartScanner::SetBoundary(StringRef boundary) { + m_boundaryWith = "\n--"; + m_boundaryWith += boundary; + m_boundaryWithout = "\n"; + m_boundaryWithout += boundary; + m_dashes = kUnknown; +} + +void HttpMultipartScanner::Reset(bool saveSkipped) { + m_saveSkipped = saveSkipped; + m_state = kBoundary; + m_posWith = 0; + m_posWithout = 0; + m_buf.resize(0); +} + +StringRef HttpMultipartScanner::Execute(StringRef in) { + if (m_state == kDone) Reset(m_saveSkipped); + if (m_saveSkipped) m_buf += in; + + size_t pos = 0; + if (m_state == kBoundary) { + for (char ch : in) { + ++pos; + if (m_dashes != kWithout) { + if (ch == m_boundaryWith[m_posWith]) { + ++m_posWith; + if (m_posWith == m_boundaryWith.size()) { + // Found the boundary; transition to padding + m_state = kPadding; + m_dashes = kWith; // no longer accept plain 'boundary' + break; + } + } else if (ch == m_boundaryWith[0]) { + m_posWith = 1; + } else { + m_posWith = 0; + } + } + + if (m_dashes != kWith) { + if (ch == m_boundaryWithout[m_posWithout]) { + ++m_posWithout; + if (m_posWithout == m_boundaryWithout.size()) { + // Found the boundary; transition to padding + m_state = kPadding; + m_dashes = kWithout; // no longer accept '--boundary' + break; + } + } else if (ch == m_boundaryWithout[0]) { + m_posWithout = 1; + } else { + m_posWithout = 0; + } + } + } + } + + if (m_state == kPadding) { + for (char ch : in.drop_front(pos)) { + ++pos; + if (ch == '\n') { + // Found the LF; return remaining input buffer (following it) + m_state = kDone; + if (m_saveSkipped) m_buf.resize(m_buf.size() - in.size() + pos); + return in.drop_front(pos); + } + } + } + + // We consumed the entire input + return StringRef{}; +} + } // namespace wpi diff --git a/wpiutil/src/main/native/include/wpi/HttpUtil.h b/wpiutil/src/main/native/include/wpi/HttpUtil.h index 50987054aa..b0668ebc2f 100644 --- a/wpiutil/src/main/native/include/wpi/HttpUtil.h +++ b/wpiutil/src/main/native/include/wpi/HttpUtil.h @@ -141,6 +141,49 @@ class HttpConnection { explicit operator bool() const { return stream && !is.has_error(); } }; +class HttpMultipartScanner { + public: + explicit HttpMultipartScanner(StringRef boundary, bool saveSkipped = false) { + Reset(saveSkipped); + SetBoundary(boundary); + } + + // Change the boundary. This is only safe to do when IsDone() is true (or + // immediately after construction). + void SetBoundary(StringRef boundary); + + // Reset the scanner. This allows reuse of internal buffers. + void Reset(bool saveSkipped = false); + + // Execute the scanner. Will automatically call Reset() on entry if IsDone() + // is true. + // @param in input data + // @return the input not consumed; empty if all input consumed + StringRef Execute(StringRef in); + + // Returns true when the boundary has been found. + bool IsDone() const { return m_state == kDone; } + + // Get the skipped data. Will be empty if saveSkipped was false. + StringRef GetSkipped() const { + return m_saveSkipped ? StringRef{m_buf} : StringRef{}; + } + + private: + SmallString<64> m_boundaryWith, m_boundaryWithout; + + // Internal state + enum State { kBoundary, kPadding, kDone }; + State m_state; + size_t m_posWith, m_posWithout; + enum Dashes { kUnknown, kWith, kWithout }; + Dashes m_dashes; + + // Buffer + bool m_saveSkipped; + std::string m_buf; +}; + } // namespace wpi #include "HttpUtil.inl" diff --git a/wpiutil/src/test/native/cpp/HttpUtilTest.cpp b/wpiutil/src/test/native/cpp/HttpUtilTest.cpp new file mode 100644 index 0000000000..a83214d077 --- /dev/null +++ b/wpiutil/src/test/native/cpp/HttpUtilTest.cpp @@ -0,0 +1,105 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2018 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#include "wpi/HttpUtil.h" // NOLINT(build/include_order) + +#include "gtest/gtest.h" + +namespace wpi { + +TEST(HttpMultipartScannerTest, ExecuteExact) { + HttpMultipartScanner scanner("foo"); + EXPECT_TRUE(scanner.Execute("abcdefg---\r\n--foo\r\n").empty()); + EXPECT_TRUE(scanner.IsDone()); + EXPECT_TRUE(scanner.GetSkipped().empty()); +} + +TEST(HttpMultipartScannerTest, ExecutePartial) { + HttpMultipartScanner scanner("foo"); + EXPECT_TRUE(scanner.Execute("abcdefg--").empty()); + EXPECT_FALSE(scanner.IsDone()); + EXPECT_TRUE(scanner.Execute("-\r\n").empty()); + EXPECT_FALSE(scanner.IsDone()); + EXPECT_TRUE(scanner.Execute("--foo\r").empty()); + EXPECT_FALSE(scanner.IsDone()); + EXPECT_TRUE(scanner.Execute("\n").empty()); + EXPECT_TRUE(scanner.IsDone()); +} + +TEST(HttpMultipartScannerTest, ExecuteTrailing) { + HttpMultipartScanner scanner("foo"); + EXPECT_EQ(scanner.Execute("abcdefg---\r\n--foo\r\nxyz"), "xyz"); +} + +TEST(HttpMultipartScannerTest, ExecutePadding) { + HttpMultipartScanner scanner("foo"); + EXPECT_EQ(scanner.Execute("abcdefg---\r\n--foo \r\nxyz"), "xyz"); + EXPECT_TRUE(scanner.IsDone()); +} + +TEST(HttpMultipartScannerTest, SaveSkipped) { + HttpMultipartScanner scanner("foo", true); + scanner.Execute("abcdefg---\r\n--foo\r\n"); + EXPECT_EQ(scanner.GetSkipped(), "abcdefg---\r\n--foo\r\n"); +} + +TEST(HttpMultipartScannerTest, Reset) { + HttpMultipartScanner scanner("foo", true); + + scanner.Execute("abcdefg---\r\n--foo\r\n"); + EXPECT_TRUE(scanner.IsDone()); + EXPECT_EQ(scanner.GetSkipped(), "abcdefg---\r\n--foo\r\n"); + + scanner.Reset(true); + EXPECT_FALSE(scanner.IsDone()); + scanner.SetBoundary("bar"); + + scanner.Execute("--foo\r\n--bar\r\n"); + EXPECT_TRUE(scanner.IsDone()); + EXPECT_EQ(scanner.GetSkipped(), "--foo\r\n--bar\r\n"); +} + +TEST(HttpMultipartScannerTest, WithoutDashes) { + HttpMultipartScanner scanner("foo", true); + + EXPECT_TRUE(scanner.Execute("--\r\nfoo\r\n").empty()); + EXPECT_TRUE(scanner.IsDone()); +} + +TEST(HttpMultipartScannerTest, SeqDashesDashes) { + HttpMultipartScanner scanner("foo", true); + EXPECT_TRUE(scanner.Execute("\r\n--foo\r\n").empty()); + EXPECT_TRUE(scanner.IsDone()); + EXPECT_TRUE(scanner.Execute("\r\n--foo\r\n").empty()); + EXPECT_TRUE(scanner.IsDone()); +} + +TEST(HttpMultipartScannerTest, SeqDashesNoDashes) { + HttpMultipartScanner scanner("foo", true); + EXPECT_TRUE(scanner.Execute("\r\n--foo\r\n").empty()); + EXPECT_TRUE(scanner.IsDone()); + EXPECT_TRUE(scanner.Execute("\r\nfoo\r\n").empty()); + EXPECT_FALSE(scanner.IsDone()); +} + +TEST(HttpMultipartScannerTest, SeqNoDashesDashes) { + HttpMultipartScanner scanner("foo", true); + EXPECT_TRUE(scanner.Execute("\r\nfoo\r\n").empty()); + EXPECT_TRUE(scanner.IsDone()); + EXPECT_TRUE(scanner.Execute("\r\n--foo\r\n").empty()); + EXPECT_FALSE(scanner.IsDone()); +} + +TEST(HttpMultipartScannerTest, SeqNoDashesNoDashes) { + HttpMultipartScanner scanner("foo", true); + EXPECT_TRUE(scanner.Execute("\r\nfoo\r\n").empty()); + EXPECT_TRUE(scanner.IsDone()); + EXPECT_TRUE(scanner.Execute("\r\nfoo\r\n").empty()); + EXPECT_TRUE(scanner.IsDone()); +} + +} // namespace wpi